diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1514e89d5..9644d056cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,15 +73,6 @@ android { } } - buildTypes { - debug { - buildConfigField("String", "BUILD_TYPE", "\"debug\"") - } - release { - buildConfigField("String", "BUILD_TYPE", "\"release\"") - } - } - } configurations.all { @@ -152,6 +143,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) @@ -304,6 +296,8 @@ dependencies { implementation(projects.features.tokenRecieve.impl) implementation(projects.features.yieldSupply.api) implementation(projects.features.yieldSupply.impl) + implementation(projects.features.approval.api) + implementation(projects.features.approval.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -380,6 +374,8 @@ dependencies { implementation(deps.amplitude) implementation(deps.appsflyer) implementation(deps.appsflyer.oaid) + implementation(deps.customerio.analytics) + implementation(deps.customerio.messaging) implementation("com.android.installreferrer:installreferrer:2.2") implementation(deps.spongecastle.core) implementation(deps.lottie) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index a7aa7666e3..cb51a8d6be 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -146,12 +146,8 @@ abstract class BaseTestCase : TestCase( return ApplicationInjectionExecutionRule( toggleStates = mapOf( "SWAP_REDESIGN_ENABLED" to false, - "NEW_ONRAMP_MAIN_ENABLED" to true, - "HOT_WALLET_ENABLED" to true, - "YIELD_SUPPLY_FEATURE_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, - "FEED_ENABLED" to true, - "GASLESS_TRANSACTIONS_ENABLED" to true, + "GASLESS_APPROVAL_ENABLED" to true, ) ) } 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 004e367549..b91b4767eb 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -18,3 +18,19 @@ fun KNode.assertTextContainsSafe( ) } +fun KNode.isDisplayedSafely(): Boolean { + return try { + assertIsDisplayed() + true + } catch (_: AssertionError) { + false + } +} + +fun KNode.assertVisibility(shouldBeDisplayed: Boolean) { + if (shouldBeDisplayed) { + assertIsDisplayed() + } else { + assertIsNotDisplayed() + } +} diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index bc4f4593d7..09f4b7bd1e 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.screens.* -import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton import com.tangem.tap.domain.sdk.mocks.MockContent import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.utils.StringsSigns.DASH_SIGN @@ -14,7 +13,6 @@ import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.scanCard( productType: ProductType? = null, mockContent: MockContent? = null, - alreadyActivatedDialogIsShown: Boolean = false, isTwinsCard: Boolean = false, ) { if (productType != null) { @@ -32,12 +30,6 @@ fun BaseTestCase.scanCard( step("Click on 'Scan card or ring' button") { onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } } - if (alreadyActivatedDialogIsShown) { - step("Click on 'This is my wallet' button") { - waitForIdle() - AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() } - } - } if (isTwinsCard) { step("Click on 'Continue' button") { onOnboardingScreen { continueButton.clickWithAssertion() } @@ -51,14 +43,12 @@ fun BaseTestCase.scanCard( fun BaseTestCase.openMainScreen( productType: ProductType? = null, mockContent: MockContent? = null, - alreadyActivatedDialogIsShown: Boolean = false, isTwinsCard: Boolean = false, ) { step("Scan card") { scanCard( productType = productType, mockContent = mockContent, - alreadyActivatedDialogIsShown = alreadyActivatedDialogIsShown, isTwinsCard = isTwinsCard, ) } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt index f90275a934..7abb486367 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt @@ -1,8 +1,8 @@ package com.tangem.scenarios import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.assertVisibility import com.tangem.screens.onSendConfirmScreen -import io.github.kakaocup.compose.node.element.KNode import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.checkSendWarning( @@ -45,12 +45,4 @@ fun BaseTestCase.checkSendWarning( sendButton.assertIsEnabled() } } -} - -private fun KNode.assertVisibility(shouldBeDisplayed: Boolean) { - if (shouldBeDisplayed) { - assertIsDisplayed() - } else { - assertIsNotDisplayed() - } } \ 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 index a86ca56854..358b29b5b3 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -2,7 +2,9 @@ package com.tangem.scenarios import androidx.compose.ui.test.click import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.assertVisibility import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.isDisplayedSafely import com.tangem.screens.* import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.Allure.step @@ -170,6 +172,131 @@ fun BaseTestCase.checkStoriesChanges() { } } +fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) { + step("Click on 'Select fee' icon") { + onSwapTokenScreen { selectFeeIcon.performClick() } + } + + when (feeType) { + FeeType.Market -> { + step("Click on 'Market' item") { + onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + FeeType.Fast -> { + step("Click on 'Fast' item") { + onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + } +} + +fun BaseTestCase.selectFeeTypeWithGasless(feeType: FeeType, selectedFeeAmount: String) { + step("Click on 'Select fee' icon") { + onSwapTokenScreen { selectFeeIcon.performClick() } + } + + when (feeType) { + FeeType.Market -> selectMarketFee(selectedFeeAmount) + FeeType.Fast -> selectFastFee(selectedFeeAmount) + } +} + +private fun BaseTestCase.selectMarketFee(selectedFeeAmount: String) { + step("Deselect current fee and select 'Market'") { + onSwapSelectNetworkFeeBottomSheet { + if (fastSelectorItem.isDisplayedSafely()) { + fastSelectorItem.performClick() + } + marketSelectorItem.performClick() + } + } + step("Click on 'Apply' button") { + onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() } + } + step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } +} + +private fun BaseTestCase.selectFastFee(selectedFeeAmount: String) { + step("Deselect current fee and select 'Fast'") { + onSwapSelectNetworkFeeBottomSheet { + if (marketSelectorItem.isDisplayedSafely()) { + marketSelectorItem.performClick() + } + fastSelectorItem.performClick() + } + } + step("Click on 'Apply' button") { + onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() } + } + step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } +} + +fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) { + step("Assert 'Unable to cover '$networkName' fee notification title is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationTitle(networkName).assertIsDisplayed() } + } + step("Assert 'Unable to cover '$networkName' fee notification text is displayed'") { + onSwapTokenScreen { + unableToCoverFeeNotificationText( + currencyName = networkName, + currencySymbol = currencySymbol + ).assertIsDisplayed() + } + } + step("Assert 'Unable to cover '$networkName' fee notification icon is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationIcon(networkName).assertIsDisplayed() } + } +} + +fun BaseTestCase.checkSwapWarning( + title: String, + message: String, + isDisplayed: Boolean = true, + swapButtonIsDisabled: Boolean = isDisplayed, +) { + val assertDisplay = if (isDisplayed) "displayed" else "not displayed" + + step("Assert warning title is $assertDisplay") { + onSwapTokenScreen { + warningTitle(title).assertVisibility(isDisplayed) + } + } + step("Assert warning icon is $assertDisplay") { + onSwapTokenScreen { + warningIcon(message).assertVisibility(isDisplayed) + } + } + step("Assert warning message is $assertDisplay") { + onSwapTokenScreen { + warningMessage(message).assertVisibility(isDisplayed) + } + } + + if (swapButtonIsDisabled) + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { + swapButton.assertIsNotEnabled() + } + } + else + step("Assert 'Swap' button is enabled") { + onSwapTokenScreen { + swapButton.assertIsEnabled() + } + } +} + sealed class SwapEntryPoint { object MainScreen : SwapEntryPoint() object TokenDetails : SwapEntryPoint() @@ -177,4 +304,9 @@ sealed class SwapEntryPoint { object TokenActionsBottomSheet : SwapEntryPoint() } +enum class FeeType { + Market, + Fast +} + diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt index e8e256017c..6da3d7a7f5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -3,7 +3,8 @@ 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 com.tangem.core.ui.test.AppBarWithSearchTestTags +import com.tangem.core.ui.test.BuyTokenScreenTestTags 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 @@ -21,11 +22,30 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv hasText(getResourceString(R.string.exchange_tokens_available_tokens_header)) } - fun tokenWithTitle(tokenTitle: String): KNode = child { - hasTestTag(TokenElementsTestTags.TOKEN_TITLE) - hasAnyDescendant(withText(tokenTitle)) - useUnmergedTree = true + val searchIcon: KNode = child { + hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON) + } + + val searchTextField: KNode = child { + hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD) + } + + val noTokensFoundText: KNode = child { + hasText(getResourceString(R.string.express_token_list_empty_search)) + } + + fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasAnyDescendant(withText(tokenTitle)) + if (!availableForSwap) { + hasAnyDescendant( + withText( + getResourceString(R.string.tokens_list_unavailable_to_swap_source_header) + ) + ) } + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt index 6705194521..007cfe289c 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt @@ -2,6 +2,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen @@ -33,6 +34,12 @@ class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNode hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT) useUnmergedTree = true } + + val applyButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_apply)) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 15be0dee77..30fe6823bd 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -43,6 +43,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val feeAmount: KNode = child { + hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT)) + useUnmergedTree = true + } + val receiveAmountShimmer: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) } @@ -71,6 +76,61 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + fun unableToCoverFeeNotificationTitle(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationText(currencyName: String, currencySymbol: String): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_description, + currencyName, + currencySymbol + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationIcon(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withTestTag(NotificationTestTags.TITLE)) + hasAnySibling( + withText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName, + ) + ) + ) + useUnmergedTree = true + } + + fun warningTitle(title: String): KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText(title) + useUnmergedTree = true + } + + fun warningMessage(message: String): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText(message) + useUnmergedTree = true + } + + fun warningIcon(message: String): KNode = child { + hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withText(message)) + useUnmergedTree = true + } + val refreshButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.warning_button_refresh)) @@ -95,15 +155,30 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val insufficientFundsErrorTitle: KNode = child { + hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE) + hasText(getResourceString(R.string.swapping_insufficient_funds)) + useUnmergedTree = true + } + val receiveFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) } + val receiveFiatAmountWithPriceImpactWarning: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING) + } + + val receiveFiatAmountInformationIcon: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON) + useUnmergedTree = true + } + val swapFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT) } - val changeTokenIcon: KNode = child { + val selectTokenIcon: KNode = child { hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index c084d381c8..54604ff5cf 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -64,7 +64,7 @@ class DetailsTest : BaseTestCase() { fun wallet2DetailsTest() = setupHooks().run { step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = ProductType.Wallet2) } onTopBar { step("Open wallet details") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 0a20f32754..a1cc2319c9 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -6,11 +6,12 @@ import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.redux.StateDialog -import com.tangem.scenarios.* +import com.tangem.scenarios.checkFailedTransactionDialog +import com.tangem.scenarios.checkScanWarningDialog +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.* -import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.store @@ -96,7 +97,7 @@ class FeedbackTest : BaseTestCase() { step("Click 'Next' button") { onSendAddressScreen { nextButton.clickWithAssertion() } } - step("Assert sanding text is displayed") { + step("Assert sеnding text is displayed") { onSendConfirmScreen { sendingText.assertIsDisplayed() } } step("Click 'Send' button") { @@ -163,39 +164,4 @@ class FeedbackTest : BaseTestCase() { } } } - - @AllureId("3986") - @DisplayName("Send feedback: from scan already used wallet alert dialog") - @Test - fun sendFeedbackAfterScanAlreadyUsedWalletTest() { - val gmailText = "Welcome to Gmail" - - setupHooks( - additionalAfterSection = { - device.uiDevice.pressBack() - } - ).run { - step("Set mocks for Wallet2") { - MockProvider.setMocks(ProductType.Wallet2) - } - step("Click on 'Accept' button") { - onDisclaimerScreen { acceptButton.clickWithAssertion() } - } - step("Click on 'Get started' button") { - onStoriesScreen { getStartedButton.clickWithAssertion() } - } - step("Click on 'Scan card or ring' button") { - onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } - } - step("Check 'Already used Wallet' dialog") { - checkAlreadyUsedWalletDialog() - } - step("Click on 'Request support' button") { - AlreadyUsedWalletDialogPageObject { requestSupportButton.click() } - } - step("Assert 'Gmail' app is open") { - ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) } - } - } - } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt index c1e7d608de..26238b9789 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt @@ -30,10 +30,7 @@ class OnboardingTest : BaseTestCase() { @Test fun shibaBackupScreenTest() { setupHooks().run { - scanCard( - mockContent = ShibaNoBackupMockContent, - alreadyActivatedDialogIsShown = true - ) + scanCard(mockContent = ShibaNoBackupMockContent) checkBackupScreen() } } @@ -54,10 +51,7 @@ class OnboardingTest : BaseTestCase() { @Test fun wallet2BackupScreenTest() { setupHooks().run { - scanCard( - mockContent = Wallet2NoBackupMockContent, - alreadyActivatedDialogIsShown = true - ) + scanCard(mockContent = Wallet2NoBackupMockContent) checkBackupScreen() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt index 50a5f44b6d..60c2157e1a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt @@ -64,7 +64,7 @@ class ResetCardTest : BaseTestCase() { fun resetWallet2CardWithBackupTest() { setupHooks().run { step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = ProductType.Wallet2) } step("Open 'Device settings' screen") { openDeviceSettingsScreen() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt index 856a3963b3..1b83533fcb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt @@ -73,7 +73,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on $card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen( @@ -119,7 +119,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$card' card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) @@ -138,7 +138,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$ring'") { - openMainScreen(productType = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = cardType) } step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) @@ -175,7 +175,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$card' card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt index 9bf7545be1..105eac0ecc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt @@ -1,9 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.openMainScreen @@ -22,30 +20,32 @@ class SendTest : BaseTestCase() { @DisplayName("Send: check fee notification") @Test fun checkFeeNotificationTest() { - val currencyName = "POL (ex-MATIC)" - val feeCurrencyName = "Ethereum" - val feeCurrencySymbol = "ETH" - val scenarioName = "eth_network_balance" - val scenarioState = "Empty" + val currencyName = "USDC" + val feeCurrencyName = "Solana" + val feeCurrencySymbol = "SOL" + val balanceScenarioName = "solana_balance" + val tokensScenarioName = "user_tokens_api" + val balanceState = "Empty" + val tokensState = "SolanaUSDC" setupHooks( additionalAfterSection = { - resetWireMockScenarioState(scenarioName) + resetWireMockScenarioState(balanceScenarioName) + resetWireMockScenarioState(tokensScenarioName) } ).run { - step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { - setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") { + setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState) } - step("Open 'Main Screen'") { openMainScreen() } step("Synchronize addresses") { synchronizeAddresses() } - step("Swipe up") { - swipeVertical(SwipeDirection.UP) - } step("Click on token with name: $currencyName") { onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt index cf0024f8bb..8bbaf0ed91 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt @@ -75,7 +75,7 @@ class WarningTest : BaseTestCase() { } ).run { step("Open 'Main' screen") { - openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent) } step("Assert 'Seed phrase' notification icon is displayed") { onMainScreen { seedPhraseNotificationIcon.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 5a158f6e07..f634fe21c0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt @@ -2,17 +2,16 @@ 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 -import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.checkQrCodeBottomSheetScenario import com.tangem.scenarios.goToQrCodeBottomSheet import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.synchronizeAddresses -import com.tangem.screens.* +import com.tangem.screens.onMainScreen +import com.tangem.screens.onSwapStoriesScreen +import com.tangem.screens.onSwapTokenScreen +import com.tangem.screens.onTokenDetailsScreen import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -124,97 +123,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { } } - @AllureId("4460") - @DisplayName("Action buttons (token details screen): check 'Swap' button (provider error)") - @Test - fun checkSwapButtonProviderErrorTest() { - val tokenTitle = "POL (ex-MATIC)" - - setupHooks().run { - step("Open 'Main Screen'") { - openMainScreen() - } - step("Synchronize addresses") { - synchronizeAddresses() - } - step("Swipe up") { - swipeVertical(SwipeDirection.UP) - } - step("Click on token with name: '$tokenTitle'") { - waitForIdle() - onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } - } - step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertIsDimmed() } - } - step("Click on 'Swap' button") { - onTokenDetailsScreen { swapButton().performClick() } - } - step("Assert swapping $tokenTitle is not supported dialog text is displayed") { - onSwapIsNotSupportedDialog { text(tokenTitle).assertIsDisplayed() } - } - step("Assert 'Ok' button is displayed") { - onSwapIsNotSupportedDialog { okButton.assertIsDisplayed() } - } - step("Click on 'Ok' button") { - onSwapIsNotSupportedDialog { okButton.performClick() } - } - step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertIsDimmed() } - } - } - } - - @AllureId("4461") - @DisplayName("Action buttons (token details screen): check 'Swap' button (Express error)") - @Test - fun checkSwapButtonExpressErrorTest() { - val tokenTitle = "Polygon" - val scenarioName = "express_api_assets" - val scenarioState = "Error" - - setupHooks( - additionalAfterSection = { - resetWireMockScenarioState(scenarioName) - } - ).run { - step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { - setWireMockScenarioState(scenarioName, scenarioState) - } - step("Open 'Main Screen'") { - openMainScreen() - } - step("Synchronize addresses") { - synchronizeAddresses() - } - step("Swipe up") { - swipeVertical(SwipeDirection.UP) - } - step("Click on token with name: '$tokenTitle'") { - waitForIdle() - onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } - } - step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertIsDimmed() } - } - step("Click on 'Swap' button") { - onTokenDetailsScreen { swapButton().performClick() } - } - step("Assert operation is unavailable dialog text is displayed") { - onOperationIsUnavailableDialog { text.assertIsDisplayed() } - } - step("Assert 'Ok' button is displayed") { - onOperationIsUnavailableDialog { okButton.assertIsDisplayed() } - } - step("Click on 'Ok' button") { - onOperationIsUnavailableDialog { okButton.performClick() } - } - step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertIsDimmed() } - } - } - } - @AllureId("3590") @DisplayName("Action buttons (token details screen): validate UI") @Test diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt index c545a4100b..5105441296 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt @@ -23,7 +23,7 @@ class KusamaWarningsTest : BaseTestCase() { private val tokenName = "Kusama" private val amountToLeaveLessThanDeposit = "0.300333" private val amountToLeaveGreaterThanDeposit = "0.1" - private val depositAmount = "KSM 0.000333333333" + private val depositAmount = "KSM 0.000003333" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt index be40b64a78..31cf64adce 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -23,7 +23,7 @@ class PolkadotWarningsTest : BaseTestCase() { private val tokenName = "Polkadot" private val amountToLeaveLessThanDeposit = "1.299" private val amountToLeaveGreaterThanDeposit = "0.2" - private val depositAmount = "DOT 1.00" + private val depositAmount = "DOT 0.01" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt new file mode 100644 index 0000000000..f84fdea929 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt @@ -0,0 +1,177 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +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 +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapChooseTokenScreenTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8505") + @DisplayName("Swap: check available to swap tokens list") + @Test + fun checkAvailableToSwapTokensListTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val bitcoin = "Bitcoin" + val scenarioState = "CustomTokenAndJesusAdded" + val jesusCoin = "Jesus Coin" + val salam = "Salam" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { selectTokenIcon.performClick() } + } + step("Assert '$ethereum' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Assert '$bitcoin' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() } + } + step("Assert '$jesusCoin' is displayed and unavailable for swap") { + onSwapChooseTokenScreen { tokenWithTitle(tokenTitle = jesusCoin).assertIsDisplayed() } + } + step("Assert custom token without backend id '$salam' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8506") + @DisplayName("Swap: check search on choose swap token screen") + @Test + fun checkSearchOnSwapChooseTokenScreenTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val polExMaticSymbol = "POL" + val invalidSearchText = "f" + val validSearchText = "pol" + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { selectTokenIcon.performClick() } + } + step("Click on 'Search' icon") { + onSwapChooseTokenScreen { searchIcon.performClick() } + } + step("Click on 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performClick() } + } + step("Type invalid search text: '$invalidSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() } + } + step("Press 'Delete' button") { + device.uiDevice.pressDelete() + } + step("Type valid search text: '$validSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Select new receive token: $polExMatic") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() } + } + step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index 2048e0fa7e..095b1ccacb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -4,16 +4,16 @@ import androidx.compose.ui.test.hasText import com.tangem.common.BaseTestCase import com.tangem.common.annotations.ApiEnv import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarios +import com.tangem.common.utils.setWireMockScenarioState 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.scenarios.* import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId @@ -266,6 +266,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("2828") @DisplayName("Swap: network fee") @Test @@ -317,6 +320,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("575") @DisplayName("Swap: check UI") @Test @@ -354,7 +360,7 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } } step("Click on 'Select token' icon") { - onSwapTokenScreen { changeTokenIcon.performClick() } + onSwapTokenScreen { selectTokenIcon.performClick() } } step("Select new receive token: $newReceiveToken") { onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() } @@ -401,6 +407,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("5162") @DisplayName("Swap: check swap tokens switch") @Test @@ -444,4 +453,262 @@ class SwapTokenScreenTest : BaseTestCase() { } } } + + @AllureId("573") + @DisplayName("Swap: check 'Swap' button availability") + @Test + fun checkSwapButtonAvailabilityTest() { + val polygon = "Polygon" + val bitcoin = "Bitcoin" + val salam = "Salam" + val scenarioState = "CustomTokenAndJesusAdded" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$polygon'") { + onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() } + } + step("Assert 'Swap' button is not dimmed. Swap available") { + onTokenDetailsScreen { swapButton().assertIsDimmed(false) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on token with name: '$bitcoin'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on unknown custom token with name: '$salam'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("583") + @DisplayName("Swap: check switch fee type (enable to cover 'Market' and 'Fast' fee)") + @Test + fun enableToCoverMarketAndFastFeeTest() { + val tokenName = "Ethereum" + val inputAmount = "0.99" + val market = "Market" + val fast = "Fast" + val marketFeeAmount = "$1.12" + val fastFeeAmount = "$1.43" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$market' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Select '$fast' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8536") + @DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)") + @Test + fun unableToCoverMarketAndFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "0.0001" + val marketFeeType = "Market" + val fastFeeType = "Fast" + val feeAmount = "$" + val scenarioName = "eth_network_balance" + val scenarioState = "LessThanDollar" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$marketFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(feeType = FeeType.Market, feeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$fastFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(feeType = FeeType.Fast, feeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("8537") + @DisplayName("Swap: check switch fee type (unable to cover 'Fast' fee)") + @Test + fun unableToCoverFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "3000" + val fastFeeType = "Fast" + val fastFeeAmount = "$2," + val marketFeeType = "Market" + val marketFeeAmount = "$1." + val scenarioName = "eth_fee_history" + val scenarioState = "UnableToCoverFastFee" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Swap' button is enabled") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + step("Select '$fastFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeTypeWithGasless(feeType = FeeType.Fast, fastFeeAmount) + } + } + step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(fastFeeAmount, substring = true) } + } + step("Check 'Unable to cover '$networkName' fee notification") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$marketFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeTypeWithGasless(feeType = FeeType.Market, marketFeeAmount) + } + } + step("Assert 'Swap' button is enabled") { + waitForIdle() + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt new file mode 100644 index 0000000000..ba385763a9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt @@ -0,0 +1,580 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +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.chackUnableToCoverFeeNotification +import com.tangem.scenarios.checkSwapWarning +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 +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapTokenScreenWarningsTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("580") + @DisplayName("Swap: check 'Insufficient funds' warning") + @Test + fun checkSwapInsufficientFundsWarningTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Insufficient funds' error is displayed") { + waitForIdle() + onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() } + } + } + } + + @AllureId("8502") + @DisplayName("Swap: check 'Unable to cover network fee' warning") + @Test + fun checkUnableToCoverBlockchainFeeWarningTest() { + val tokenTitle = "USDC" + val inputAmount = "1000" + val tokensScenarioState = "SolanaUSDC" + val balanceScenarioName = "solana_balance" + val balanceScenarioState = "Empty" + val networkName = "Solana" + val currencySymbol = "SOL" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(balanceScenarioName) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("8503") + @DisplayName("Swap: check 'High price impact' warning on CEX") + @Test + fun checkHighPriceImpactWarningCEXTest() { + val tokenTitle = "USDC" + val inputAmount = "100" + val currencySymbol = "SOL" + val slippagePercent = "5%" + val tokensScenarioState = "SolanaUSDC" + val exchangeQuoteScenarioName = "exchange_quote_solana" + val exchangeQuoteScenarioState = "HighPriceImpact" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val dialogText = getResourceString( + R.string.swapping_alert_cex_description_with_slippage, + currencySymbol, + slippagePercent + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(exchangeQuoteScenarioName) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$exchangeQuoteScenarioName' to state: $exchangeQuoteScenarioState") { + setWireMockScenarioState( + scenarioName = exchangeQuoteScenarioName, + state = exchangeQuoteScenarioState + ) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onSwapTokenScreen { + waitForIdle() + receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) + } + } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for CEX is displayed") { + onDialog { text.assertTextEquals(dialogText) } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + } + } + + @AllureId("8504") + @DisplayName("Swap: check 'High price impact' warning on DEX") + @Test + fun checkHighPriceImpactWarningDEXTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + val slippagePercent = "3.5%" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val highPriceImpactDescription = getResourceString(R.string.swapping_high_price_impact_description) + val swappingAlertDEXDescription = getResourceString(R.string.swapping_alert_dex_description) + val swappingAlertDEXDescriptionWithSlippage = getResourceString( + R.string.swapping_alert_dex_description_with_slippage, + slippagePercent + ) + val pairsToScenarioName = "polygon_pos_to_pairs" + val pairsFromScenarioName = "polygon_pos_from_pairs" + val scenarioState = "DexProvider" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(pairsToScenarioName) + resetWireMockScenarioState(pairsFromScenarioName) + } + ).run { + step("Set WireMock scenario: '$pairsToScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsToScenarioName, state = scenarioState) + } + step("Set WireMock scenario: '$pairsFromScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsFromScenarioName, state = scenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for DEX is displayed") { + onDialog { + text.assertTextContains(highPriceImpactDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescriptionWithSlippage, substring = true) + } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + step("Click on 'OK' button") { + onDialog { okButton.performClick() } + } + } + } + + @AllureId("2831") + @DisplayName("Swap: warning is not displayed, if remaining balance is equal to 0") + @Test + fun solanaRemainingBalanceEqualToZeroWarningTest() { + val tokenTitle = "Solana" + val inputAmount = "0.00168933" + val tokensScenarioState = "SolanaUSDC" + val rentAmount = "SOL 0.00089088" + val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title) + val notificationMessage = getResourceString( + R.string.send_notification_invalid_amount_rent_fee, + rentAmount + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Invalid amount' warning is not displayed") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + checkSwapWarning( + title = notificationTitle, + message = notificationMessage, + isDisplayed = false + ) + } + } + } + } + + @AllureId("2832") + @DisplayName("Swap: warning is not displayed, if remaining balance is equal to rent amount") + @Test + fun solanaRemainingBalanceEqualToRentAmountTest() { + val tokenTitle = "Solana" + val inputAmount = "0.001689338" + val tokensScenarioState = "SolanaUSDC" + val rentAmount = "SOL 0.00089088" + val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title) + val notificationMessage = getResourceString( + R.string.send_notification_invalid_amount_rent_fee, + rentAmount + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Invalid amount' warning is not displayed") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + checkSwapWarning( + title = notificationTitle, + message = notificationMessage, + isDisplayed = false + ) + } + + } + } + } + + @AllureId("2833") + @DisplayName("Swap: warning is not displayed, if remaining balance more than rent amount") + @Test + fun solanaRemainingBalanceMoreThanRentAmountTest() { + val tokenTitle = "Solana" + val inputAmount = "0.0000941" + val tokensScenarioState = "SolanaUSDC" + val rentAmount = "SOL 0.00089088" + val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title) + val notificationMessage = getResourceString( + R.string.send_notification_invalid_amount_rent_fee, + rentAmount + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Invalid amount' warning is not displayed") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + checkSwapWarning( + title = notificationTitle, + message = notificationMessage, + isDisplayed = false + ) + } + } + } + } + + @AllureId("2830") + @DisplayName("Swap: warning is displayed, if remaining balance less than rent amount") + @Test + fun solanaRemainingBalanceLessThanRentAmountTest() { + val tokenTitle = "Solana" + val inputAmount = "0.0016941" + val tokensScenarioState = "SolanaUSDC" + val rentAmount = "SOL 0.00089088" + val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title) + val notificationMessage = getResourceString( + R.string.send_notification_invalid_amount_rent_fee, + rentAmount + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + + 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 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Invalid amount' warning is displayed") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + checkSwapWarning( + title = notificationTitle, + message = notificationMessage + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..9878c6fe3f --- /dev/null +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/internal/res/drawable/ic_launcher_foreground.xml b/app/src/internal/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..4a9d1d1208 --- /dev/null +++ b/app/src/internal/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 827750b5a0..636d7a8aa0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 827750b5a0ef7202120b0df0ae903d70e18e3d32 +Subproject commit 636d7a8aa0e330e9b95e91d85f23ad15ac6d913f diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 97f825c6ba..54e4f52423 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -43,12 +43,12 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -58,12 +58,12 @@ import dagger.hilt.components.SingletonComponent @Suppress("TooManyFunctions") interface ApplicationEntryPoint { - fun getEnvironmentConfigStorage(): EnvironmentConfigStorage - fun getAppStateHolder(): AppStateHolder fun getIssuersConfigStorage(): IssuersConfigStorage + fun getEnvironmentConfig(): EnvironmentConfig + fun getFeatureTogglesManager(): FeatureTogglesManager fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager @@ -120,8 +120,6 @@ interface ApplicationEntryPoint { fun getOnboardingRepository(): OnboardingRepository - fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider - fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore @@ -154,4 +152,6 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory + + fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index cf540d0ec0..ea612e1a2d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService @@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase - @Inject - internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles - private val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index a22cc0dd22..3fe333e207 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -62,34 +61,32 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.tap.common.analytics.AnalyticsFactory +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient +import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.rekotlin.Store import timber.log.Timber lateinit var store: Store val foregroundActivityObserver = ForegroundActivityObserver -internal lateinit var derivationsFinder: DerivationsFinder open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { @@ -100,12 +97,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appStateHolder: AppStateHolder get() = entryPoint.getAppStateHolder() - private val environmentConfigStorage: EnvironmentConfigStorage - get() = entryPoint.getEnvironmentConfigStorage() - private val issuersConfigStorage: IssuersConfigStorage get() = entryPoint.getIssuersConfigStorage() + private val environmentConfig: EnvironmentConfig + get() = entryPoint.getEnvironmentConfig() + private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() @@ -190,9 +187,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val onboardingRepository: OnboardingRepository get() = entryPoint.getOnboardingRepository() - private val dispatchers: CoroutineDispatcherProvider - get() = entryPoint.getCoroutineDispatcherProvider() - private val excludedBlockchains: ExcludedBlockchains get() = entryPoint.getExcludedBlockchains() @@ -246,6 +240,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() + private val customerIoFeatureToggles: CustomerIoFeatureToggles + get() = entryPoint.getCustomerIoFeatureToggles() + // endregion private val appScope = MainScope() @@ -312,9 +309,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. Timber.i(excludedBlockchainsManager.toString()) } - runBlocking { - initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) - } + initWithConfigDependency(environmentConfig = environmentConfig) abTestsManager.init() @@ -348,15 +343,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - derivationsFinder = DerivationsFinder( - userTokensResponseStore = userTokensResponseStore, - dispatchers = dispatchers, - ) - appStateHolder.mainStore = store wcInitializeUseCase.init( - projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, + projectId = environmentConfig.walletConnectProjectId, ) } @@ -387,7 +377,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - environmentConfigStorage = environmentConfigStorage, onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, @@ -427,6 +416,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory)) + if (customerIoFeatureToggles.isFeatureEnabled) { + factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) + } + factory.addFilter(oneTimeEventFilter) factory.addFilter(AppsFlyerEventFilter()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt new file mode 100644 index 0000000000..ae70817e3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +class CustomerIoFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "CUSTOMER_IO_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt new file mode 100644 index 0000000000..5c81ccfd73 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.UserIdHolder + +/** + * Client interface for Customer.io SDK operations. + */ +interface CustomerIoAnalyticsClient : UserIdHolder \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt new file mode 100644 index 0000000000..a86b651af3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder + +/** + * Customer.io analytics handler. + */ +class CustomerIoAnalyticsHandler( + private val client: CustomerIoAnalyticsClient, +) : AnalyticsHandler, AnalyticsUserIdHandler { + + override fun id(): String = ID + + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + + override fun send(event: AnalyticsEvent) { + // No-op: product events are not sent to Customer.io. + // Triggers are configured to come from Amplitude directly. + } + + companion object { + const val ID = "CustomerIO" + } + + class Builder : AnalyticsHandlerBuilder { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? { + val cdpApiKey = data.config.customerIoCdpApiKey + return if (data.logConfig.isCustomerIoLogEnabled) { + CustomerIoAnalyticsHandler(client = CustomerIoLogClient()) + } else if (!cdpApiKey.isNullOrBlank()) { + CustomerIoAnalyticsHandler( + client = CustomerIoClient( + application = data.application, + cdpApiKey = cdpApiKey, + ), + ) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt new file mode 100644 index 0000000000..ddd534e938 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -0,0 +1,45 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import android.app.Application +import io.customer.messagingpush.ModuleMessagingPushFCM +import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder +import io.customer.sdk.data.model.Region +import timber.log.Timber + +/** + * Real Customer.io SDK client. + * + * Initializes the SDK with the given CDP API key and provides: + * - User identification (identify / clearIdentify) + * + * Auto-tracking of application lifecycle events is disabled since it is not needed. + * Auto-tracking of screen views is disabled. + */ +internal class CustomerIoClient( + application: Application, + cdpApiKey: String, +) : CustomerIoAnalyticsClient { + + init { + CustomerIOBuilder( + applicationContext = application, + cdpApiKey = cdpApiKey, + ) + .region(Region.EU) + .trackApplicationLifecycleEvents(false) + .autoTrackActivityScreens(false) + .addCustomerIOModule(ModuleMessagingPushFCM()) + .build() + + Timber.d("CustomerIO SDK initialized") + } + + override fun setUserId(userId: String) { + CustomerIO.instance().identify(userId = userId) + } + + override fun clearUserId() { + CustomerIO.instance().clearIdentify() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt new file mode 100644 index 0000000000..62a36d09f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import timber.log.Timber + +/** + * Log client for Customer.io (used in debug mode). + * + * Logs all operations to Timber instead of sending them to Customer.io. + */ +internal class CustomerIoLogClient : CustomerIoAnalyticsClient { + + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId") + } + + override fun clearUserId() { + Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId") + this.userId = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 97026725ea..714a4acfd7 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -3,11 +3,19 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles +import dagger.hilt.android.AndroidEntryPoint +import io.customer.messagingpush.CustomerIOFirebaseMessagingService import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint @SuppressLint("MissingFirebaseInstanceTokenRefresh") internal class TangemPushNotificationService : FirebaseMessagingService() { + @Inject + lateinit var customerIoFeatureToggles: CustomerIoFeatureToggles + private val pushNotificationDelegate: PushNotificationDelegate by lazy { PushNotificationDelegate(applicationContext) } @@ -15,11 +23,19 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) Timber.d("New FCM token received: $token") + + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) + } } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + } + val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index b43504d06c..ae347d0807 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware -import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -29,7 +28,6 @@ data class AppState( AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, LegacyMiddleware.legacyMiddleware, - TradeCryptoMiddleware.middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt new file mode 100644 index 0000000000..89c89aef25 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.data + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.offramp.repository.OfframpRepository +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService + +/** + * Default implementation of [OfframpRepository] + * + * @property sellService sell service for getting offramp URL + */ +internal class DefaultOfframpRepository( + private val sellService: SellService, +) : OfframpRepository { + + override fun getOfframpUrl( + cryptoCurrency: CryptoCurrency, + fiatCurrencyCode: String, + walletAddress: String, + ): String? { + return sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + ) + } +} \ No newline at end of file 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 0d9a4eba92..595d7ccdcb 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -62,23 +62,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore - .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) - } + appPreferencesStore + .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) } override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), + ) + .takeIf { !it.isNullOrEmpty() } } override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - } + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") } override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) = @@ -92,58 +88,57 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? { - val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(tokensAdapter::fromJson) + return withContext(dispatcherProvider.io) { + val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) - return authTokens?.let { tokens -> - if (tokens.idempotencyKey == null) { - val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) - storeAuthTokens(customerWalletAddress, newAuthTokens) - newAuthTokens - } else { - tokens + authTokens?.let { tokens -> + if (tokens.idempotencyKey == null) { + val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) + storeAuthTokens(customerWalletAddress, newAuthTokens) + newAuthTokens + } else { + tokens + } } } } override suspend fun clearAuthTokens(customerWalletAddress: String) { - secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + withContext(dispatcherProvider.io) { + secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + } } override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) } override suspend fun getOrderId(customerWalletAddress: String): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) + .takeIf { !it.isNullOrEmpty() } } override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), + ) == true } override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) } - override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + override suspend fun clearOrderId(customerWalletAddress: String) { appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") } override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), isPaeraCustomer) + appPreferencesStore.store( + PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), + isPaeraCustomer, + ) } override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? { @@ -152,7 +147,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_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_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -179,7 +176,9 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = appPreferencesStore.getObjectMapSync( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) return orders[createWithdrawOrderIdKey(userWalletId)] } @@ -191,7 +190,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -221,40 +222,33 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) } override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), + ) == true } override suspend fun storeTangemPayEligibility(eligibility: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) - } + appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) } override suspend fun getTangemPayEligibility(): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) - } + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) } - override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) } + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) + } private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 1f40496096..e5600cc208 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di import com.tangem.datasource.api.moonpay.MoonPayApi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager @@ -69,14 +69,14 @@ internal object ActivityModule { @Provides @Singleton fun provideExchangeService( - environmentConfigStorage: EnvironmentConfigStorage, getSelectedWalletUseCase: GetSelectedWalletUseCase, moonPayApi: MoonPayApi, + environmentConfig: EnvironmentConfig, ): SellService { return MoonPayService( api = moonPayApi, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey }, - secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey }, + apiKey = environmentConfig.moonPayApiKey, + secretKey = environmentConfig.moonPayApiSecretKey, userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() }, ) } diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 38e66db21b..53f53dffef 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di import android.content.Context +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender @@ -12,6 +13,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler @@ -40,6 +42,8 @@ internal class TangemSdkManagerModule { appFinisher: AppFinisher, sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, analyticsExceptionHandler: AnalyticsExceptionHandler, + blockchainToDeriveFinder: BlockchainToDeriveFinder, + analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -56,6 +60,8 @@ internal class TangemSdkManagerModule { appFinisher = appFinisher, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, analyticsExceptionHandler = analyticsExceptionHandler, + blockchainToDeriveFinder = blockchainToDeriveFinder, + analyticsEventHandler = analyticsEventHandler, dispatchers = dispatchers, ) } 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 569c69c6b8..1240259f77 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 @@ -1,6 +1,5 @@ package com.tangem.tap.di.domain -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier @@ -97,12 +96,10 @@ internal object AccountDomainModule { 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/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt index 3ae0f6d5c6..d6555cd643 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.supplier.MultiAccountListSupplier 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 @@ -19,15 +19,15 @@ object EarnDomainModule { } @Provides - fun provideManageEarnNetworksUseCase( + fun provideGetEarnNetworksUseCase( earnRepository: EarnRepository, + multiAccountListSupplier: MultiAccountListSupplier, userWalletsListRepository: UserWalletsListRepository, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, ): GetEarnNetworksUseCase { return GetEarnNetworksUseCase( earnRepository = earnRepository, + multiAccountListSupplier = multiAccountListSupplier, userWalletsListRepository = userWalletsListRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index b1d7b0a206..b527533fc1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -3,20 +3,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -57,40 +48,6 @@ internal object ManageTokensDomainModule { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } - @Provides - @Singleton - fun provideRemoveCustomManagedCryptoCurrencyUseCase( - customTokensRepository: CustomTokensRepository, - ): RemoveCustomManagedCryptoCurrencyUseCase { - return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository) - } - - @Provides - @Singleton - fun provideSaveManagedTokensUseCase( - customTokensRepository: CustomTokensRepository, - walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - derivationsRepository: DerivationsRepository, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - stakingIdFactory: StakingIdFactory, - dispatchers: CoroutineDispatcherProvider, - ): SaveManagedTokensUseCase { - return SaveManagedTokensUseCase( - customTokensRepository = customTokensRepository, - walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - derivationsRepository = derivationsRepository, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiStakingBalanceFetcher = multiStakingBalanceFetcher, - stakingIdFactory = stakingIdFactory, - parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), - ) - } - @Provides @Singleton fun provideGetSupportedNetworksUseCase( 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 d96861be6a..3be28c02e2 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 @@ -4,22 +4,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.StakingIdFactory -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.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -66,32 +56,6 @@ object MarketsDomainModule { return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier) } - @Provides - @Singleton - fun provideSaveMarketTokensUseCase( - derivationsRepository: DerivationsRepository, - marketsTokenRepository: MarketsTokenRepository, - walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - stakingIdFactory: StakingIdFactory, - dispatchers: CoroutineDispatcherProvider, - ): SaveMarketTokensUseCase { - return SaveMarketTokensUseCase( - derivationsRepository = derivationsRepository, - marketsTokenRepository = marketsTokenRepository, - walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiStakingBalanceFetcher = multiStakingBalanceFetcher, - stakingIdFactory = stakingIdFactory, - parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), - ) - } - @Provides @Singleton fun provideGetTokenMarketCryptoCurrency( diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index cf1a7707c6..4cbe877c01 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di.domain -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* @@ -25,15 +24,11 @@ internal object NFTDomainModule { @Provides @Singleton fun providesGetNFTCollectionsUseCase( - currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, singleAccountListSupplier: SingleAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( - currenciesRepository = currenciesRepository, nftRepository = nftRepository, singleAccountListSupplier = singleAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, ) @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 34ffd4aaba..ad1ba0be09 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,9 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.domain.offramp.GetOfframpUrlUseCase +import com.tangem.domain.offramp.repository.OfframpRepository import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.data.DefaultOfframpRepository +import com.tangem.tap.network.exchangeServices.SellService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -275,4 +279,16 @@ internal object OnrampDomainModule { promoRepository = promoRepository, ) } + + @Provides + @Singleton + fun provideOfframpRepository(sellService: SellService): OfframpRepository { + return DefaultOfframpRepository(sellService) + } + + @Provides + @Singleton + fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase { + return GetOfframpUrlUseCase(offrampRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 5bb200ad04..8e58e90e09 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher @@ -19,7 +20,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository @@ -39,28 +39,6 @@ import javax.inject.Singleton @Suppress("TooManyFunctions", "LargeClass") internal object TokensDomainModule { - @Provides - @Singleton - fun provideAddCryptoCurrenciesUseCase( - currenciesRepository: CurrenciesRepository, - walletManagersFacade: WalletManagersFacade, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleStakingBalanceFetcher: SingleStakingBalanceFetcher, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - stakingIdFactory: StakingIdFactory, - ): AddCryptoCurrenciesUseCase { - return AddCryptoCurrenciesUseCase( - currenciesRepository = currenciesRepository, - walletManagersFacade = walletManagersFacade, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleStakingBalanceFetcher = singleStakingBalanceFetcher, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - ) - } - @Provides @Singleton fun provideFetchPendingTransactionsUseCase( @@ -75,32 +53,6 @@ internal object TokensDomainModule { return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager) } - @Provides - @Singleton - fun provideGetTokenListUseCase( - currenciesRepository: CurrenciesRepository, - currenciesStatusesOperations: BaseCurrencyStatusOperations, - ): GetTokenListUseCase { - return GetTokenListUseCase( - currenciesRepository = currenciesRepository, - currenciesStatusesOperations = currenciesStatusesOperations, - ) - } - - @Provides - @Singleton - fun provideRemoveCurrencyUseCase( - currenciesRepository: CurrenciesRepository, - walletManagersFacade: WalletManagersFacade, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - ): RemoveCurrencyUseCase { - return RemoveCurrencyUseCase( - currenciesRepository = currenciesRepository, - walletManagersFacade = walletManagersFacade, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - ) - } - @Provides @Singleton fun provideGetCurrencyUseCase( @@ -113,20 +65,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetAllWalletsCryptoCurrencyStatusesUseCase( - currenciesRepository: CurrenciesRepository, - currencyStatusOperations: BaseCurrencyStatusOperations, - dispatchers: CoroutineDispatcherProvider, - ): GetAllWalletsCryptoCurrencyStatusesUseCase { - return GetAllWalletsCryptoCurrencyStatusesUseCase( - currenciesRepository = currenciesRepository, - currencyStatusOperations = currencyStatusOperations, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideGetCurrencyWarningsUseCase( @@ -176,34 +114,6 @@ internal object TokensDomainModule { return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier) } - @Provides - @Singleton - fun provideToggleTokenListGroupingUseCase( - dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCase { - return ToggleTokenListGroupingUseCase(dispatchers) - } - - @Provides - @Singleton - fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { - return ToggleTokenListSortingUseCase(dispatchers) - } - - @Provides - @Singleton - fun provideApplyTokenListSortingUseCase( - currenciesRepository: CurrenciesRepository, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - dispatchers: CoroutineDispatcherProvider, - ): ApplyTokenListSortingUseCase { - return ApplyTokenListSortingUseCase( - currenciesRepository = currenciesRepository, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideGetCryptoCurrencyActionsUseCase( @@ -308,14 +218,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetWalletTotalBalanceUseCase( - currenciesStatusesOperations: BaseCurrencyStatusOperations, - ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) - } - @Provides @Singleton fun provideRefreshMultiCurrencyWalletQuotesUseCase( @@ -350,7 +252,7 @@ internal object TokensDomainModule { multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { - return CachedCurrenciesStatusesOperations( + return BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, @@ -363,12 +265,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { - return GetCryptoCurrenciesUseCase(currenciesRepository) - } - @Provides @Singleton fun provideWalletBalanceFetcher( @@ -378,6 +274,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { @@ -388,6 +285,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) 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 5e55ebdd54..c63293cda2 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 @@ -66,6 +66,12 @@ internal object WalletsDomainModule { return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository) } + @Provides + @Singleton + fun provideGetWalletIconUseCase(walletsRepository: WalletsRepository): GetWalletIconUseCase { + return GetWalletIconUseCase(walletsRepository = walletsRepository) + } + @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( 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 aea6c8a351..caf25e2b96 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 @@ -18,7 +18,10 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.finisher.AppFinisher @@ -53,11 +56,7 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent -import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.CreateProductWalletTask -import com.tangem.tap.domain.tasks.product.ResetBackupCardTask -import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask -import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask @@ -85,6 +84,8 @@ internal class DefaultTangemSdkManager( private val appFinisher: AppFinisher, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder, + private val analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ) : TangemSdkManager { @@ -173,7 +174,7 @@ internal class DefaultTangemSdkManager( runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, - derivationsFinder = derivationsFinder, + blockchainToDeriveFinder = blockchainToDeriveFinder, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, @@ -473,6 +474,9 @@ internal class DefaultTangemSdkManager( title = resourceReference(R.string.alert_button_request_support), onClick = { coroutineScope.launch { + analyticsEventHandler.send( + Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.SignIn), + ) sendFeedbackEmailUseCase(FeedbackEmailType.BiometricsAuthenticationFailed) } }, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt new file mode 100644 index 0000000000..9118279751 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.tap.features.demo.DemoHelper +import javax.inject.Inject + +/** + * Finder of blockchains to derive. + * Returns only saved, default or demo blockchains without any additional logic + * (no cardano/ethereum additions or unnecessary blockchain removals). + */ +class BlockchainToDeriveFinder @Inject constructor( + private val walletAccountsFetcher: WalletAccountsFetcher, +) { + + suspend fun find(card: CardDTO): Set { + if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() + val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() + + val derivationStyle = card.derivationStyleProvider.getDerivationStyle() + + val blockchains = getBlockchains(userWalletId).ifEmpty { + if (DemoHelper.isDemoCardId(card.cardId)) { + getDemoBlockchains(derivationStyle, card.cardId) + } else { + getDefaultBlockchains(derivationStyle) + } + } + + return blockchains + } + + private suspend fun getBlockchains(userWalletId: UserWalletId): Set { + return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() + .flatMap { accountDTO -> + accountDTO.tokens.orEmpty() + .filter { it.contractAddress == null } + } + .mapNotNull { coin -> + val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null + val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + .toSet() + } + + private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set { + return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set { + val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun Set.mapToBlockchainsWithDerivations( + derivationStyle: DerivationStyle?, + ): Set { + return mapNotNullTo(hashSetOf()) { blockchain -> + val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null + BlockchainToDerive(blockchain, derivationPath) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt deleted file mode 100644 index eb99c14c88..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.tangem.tap.domain.tasks.product - -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -internal data class BlockchainToDerive( - val blockchain: Blockchain, - val derivationPath: DerivationPath?, -) - -// FIXME: May be move to DI, currently unnecessary -internal class DerivationsFinder( - private val userTokensResponseStore: UserTokensResponseStore, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun findBlockchainsToDerive( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): Set { - if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() - val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() - val derivationStyle = derivationStyleProvider.getDerivationStyle() - - val blockchains = withContext(dispatchers.io) { - getBlockchains(userWalletId) - }.ifEmpty { - if (DemoHelper.isDemoCardId(card.cardId)) { - getDemoBlockchains(derivationStyle, card.cardId) - } else { - getDefaultBlockchains(derivationStyle) - } - } - - // we should generate second key for cardano - // because cardano address generation for wallet2 requires keys from 2 derivations - // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ - blockchains.addSecondCardanoDerivationIfPresent() - - if (card.settings.isHDWalletAllowed) { - blockchains.addEthereumBlockchains(derivationStyle) - } - - // pay attention to this - if (!card.hasOldStyleDerivation) { - blockchains.removeUnnecessaryBlockchains() - } - - return blockchains - } - - private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens - ?: return hashSetOf() - - return responseTokens.asSequence() - .filter { it.contractAddress == null } - .mapNotNull { coin -> - val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null - val derivationPath = coin.derivationPath?.let(::DerivationPath) - - BlockchainToDerive(blockchain, derivationPath) - } - .toMutableSet() - } - - // TODO: Move to user wallet config - private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet { - return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) - } - - // TODO: Move to user wallet config - private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet { - val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) - - return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) - } -} - -private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { - val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet) - .mapToBlockchainsWithDerivations(derivationStyle) - - addAll(ethereumBlockchains) -} - -private fun MutableSet.removeUnnecessaryBlockchains() { - val unnecessaryBlockchains = listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ) - - removeAll { it.blockchain in unnecessaryBlockchains } -} - -private fun MutableSet.addSecondCardanoDerivationIfPresent() { - val cardanoDerivation = this - .firstOrNull { it.blockchain == Blockchain.Cardano } - ?.derivationPath - ?: return - - val secondCardanoBlockchain = BlockchainToDerive( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation), - ) - - add(secondCardanoBlockchain) -} - -private fun Set.mapToBlockchainsWithDerivations( - derivationStyle: DerivationStyle?, -): MutableSet { - return mapTo(hashSetOf()) { blockchain -> - BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle)) - } -} \ No newline at end of file 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 4c334fd027..eb14c89bb4 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 @@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TwinsHelper -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX @@ -45,11 +43,10 @@ import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlin.collections.set internal class ScanProductTask( private val card: Card?, - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, @@ -81,7 +78,7 @@ internal class ScanProductTask( readVisaCard( session = session, cardDto = cardDto, - scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder), callback = callback, ) return @@ -89,7 +86,7 @@ internal class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(derivationsFinder) + else -> ScanWalletProcessor(blockchainToDeriveFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { @@ -170,7 +167,7 @@ internal class ScanProductTask( } private class ScanWalletProcessor( - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -293,7 +290,6 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = getWalletProductType(card) - val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( card = card, @@ -301,8 +297,7 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = - collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = collectDerivations(card, scanResponse) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -332,32 +327,13 @@ private class ScanWalletProcessor( private suspend fun collectDerivations( card: CardDTO, - config: CardConfig, - derivationStyleProvider: DerivationStyleProvider, + scanResponse: ScanResponse, ): Map> { - val derivations = mutableMapOf>() - val blockchains = derivationsFinder - ?.findBlockchainsToDerive(card, derivationStyleProvider) - ?: return derivations + val blockchains = blockchainToDeriveFinder + ?.find(card) + ?: return emptyMap() - blockchains.forEach { blockchain -> - val curve = config.primaryCurve(blockchain.blockchain) - val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach - if (wallet.chainCode == null) return@forEach - - val key = wallet.publicKey.toMapKey() - val path = blockchain.derivationPath - if (path != null) { - val addedDerivations = derivations[key] - if (addedDerivations != null) { - derivations[key] = addedDerivations + path - } else { - derivations[key] = listOf(path) - } - } - } - - return derivations + return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 444b1f4032..4e27097a9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.tokens.TokensFeatureToggles internal class DefaultTokensFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, -) : TokensFeatureToggles \ No newline at end of file + private val featureTogglesManager: FeatureTogglesManager, +) : TokensFeatureToggles { + + override val isMultiAddressUtxoEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED") +} \ No newline at end of file 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 0f7ba29f04..c851c06bdf 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 @@ -28,7 +28,7 @@ class FinalizeTwinTask( is CompletionResult.Success -> ScanProductTask( card = readResult.data, - derivationsFinder = null, + blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, shouldCheckIsAlreadyActivated = false, 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 c75cbbbed9..80cbe137b9 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 @@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository( .map { wallets.updateWith(it) } } .doOnSuccess { loadedWallets -> - userWallets.update { toUpdate -> + userWallets.update { _ -> val selectedUserWalletId = selectedUserWalletRepository.get() selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId } ?: loadedWallets.firstOrNull()?.also { @@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository( } } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "LongMethod") override suspend fun unlock( userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod, @@ -317,7 +317,13 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> - updateWallets { it?.updateWith(sensitiveInfo) } + updateWallets { wallets -> + // It is necessary to update derivations because when scanning we obtain the missing keys + wallets?.updateWith( + walletIdToSensitiveInformation = sensitiveInfo, + walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys), + ) + } trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } 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 c9184f1335..51aefb10ab 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 @@ -1,8 +1,10 @@ package com.tangem.tap.domain.userWalletList.utils +import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation @@ -72,7 +74,10 @@ internal fun List.toUserWallets(): List return this.map { it.toUserWallet() } } -internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet { +internal fun UserWallet.updateWith( + sensitiveInformation: UserWalletSensitiveInformation, + derivedKeys: Map?, +): UserWallet { return when (this) { is UserWallet.Cold -> { copy( @@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo card = scanResponse.card.copy( wallets = requireNotNull(sensitiveInformation.wallets), ), + derivedKeys = derivedKeys ?: scanResponse.derivedKeys, // visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus, ), ) @@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo internal fun List.updateWith( walletIdToSensitiveInformation: Map, + walletIdToDerivedKeys: Map>? = null, ): List { return if (walletIdToSensitiveInformation.isEmpty()) { this } else { this.map { wallet -> - walletIdToSensitiveInformation[wallet.walletId] - ?.let(wallet::updateWith) - ?: wallet + val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId] + val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId) + + if (sensitiveInformation != null) { + wallet.updateWith(sensitiveInformation, derivedKeys) + } else { + wallet + } } } } 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 1d9a6b2f6e..22e94ae7d3 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 @@ -222,7 +222,7 @@ internal class CardSettingsModel @Inject constructor( val card = scanResponse.card modelScope.launch { - val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true + val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true store.dispatchNavigationAction { push( route = AppRoute.ResetToFactory( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt deleted file mode 100644 index 6cbc795c53..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.tap.features.wallet.redux.middlewares - -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import org.rekotlin.Middleware - -@Deprecated("Will be removed soon") -object TradeCryptoMiddleware { - - val middleware: Middleware = { _, appState -> - { nextDispatch -> - { action -> - if (action is TradeCryptoAction) { - handle(appState, action) - } - nextDispatch(action) - } - } - } - - private fun handle(state: () -> AppState?, action: TradeCryptoAction) { - if (DemoHelper.tryHandle(state)) return - - when (action) { - is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.Sell -> proceedSellAction(action) - } - } - - private fun proceedSellAction(action: TradeCryptoAction.Sell) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress - ?.defaultAddress - ?.let(NetworkAddress.Address::value) - ?: return - val currency = action.cryptoCurrencyStatus.currency - - store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl( - cryptoCurrency = currency, - fiatCurrencyName = action.appCurrencyCode, - walletAddress = networkAddress, - isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, - )?.let { url -> - store.dispatchOpenUrl(url) - Analytics.send(Token.Withdraw.ScreenOpened()) - } - } - - private fun openReceiptUrl(transactionId: String) { - store.dispatchNavigationAction(AppRouter::pop) - - val sellService = store.inject(DaggerGraphState::appStateHolder).sellService - sellService?.getSellCryptoReceiptUrl(transactionId = transactionId) - ?.let(store::dispatchOpenUrl) - } -} \ 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 1c102c9e0c..027441c202 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 @@ -3,7 +3,7 @@ package com.tangem.tap.network.auth import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.Provider @@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend internal class DefaultAuthProvider( private val userWalletsListRepository: UserWalletsListRepository, - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : AuthProvider { override suspend fun getCardPublicKey(): String { @@ -47,11 +47,11 @@ internal class DefaultAuthProvider( ApiEnvironment.DEV, ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, - -> environmentConfigStorage.getConfigSync().tangemApiKeyDev + -> environmentConfig.tangemApiKeyDev ApiEnvironment.STAGE_2, ApiEnvironment.STAGE, - -> environmentConfigStorage.getConfigSync().tangemApiKeyStage - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey + -> environmentConfig.tangemApiKeyStage + ApiEnvironment.PROD -> environmentConfig.tangemApiKey } ?: error("No tangem tech api config provided") } } @@ -60,8 +60,8 @@ internal class DefaultAuthProvider( return ProviderSuspend { when (apiEnvironment.invoke()) { ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey + -> environmentConfig.gaslessTxApiKeyDev + ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}") } ?: error("No gasless tx api config provided") } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt index 93b0595647..9ed1541c3e 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -1,15 +1,15 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider internal class DefaultP2PEthPoolAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : P2PEthPoolAuthProvider { override fun getApiKey(): String { - val keys = environmentConfigStorage.getConfigSync().p2pApiKey + val keys = environmentConfig.p2pApiKey ?: error("No P2P api keys provided") return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index ba0534ae88..079cad327a 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -1,13 +1,13 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.StakeKitAuthProvider internal class DefaultStakeKitAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : StakeKitAuthProvider { override fun getApiKey(): String { - return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided") + return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ 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 e6cba79706..6ef9e06f96 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 @@ -1,7 +1,7 @@ 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.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -22,11 +22,11 @@ internal class AuthModule { @Singleton fun provideAuthProvider( userWalletsListRepository: UserWalletsListRepository, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, ): AuthProvider { return DefaultAuthProvider( userWalletsListRepository = userWalletsListRepository, - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, ) } @@ -38,14 +38,14 @@ internal class AuthModule { @Provides @Singleton - fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider { - return DefaultStakeKitAuthProvider(environmentConfigStorage) + fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider { + return DefaultStakeKitAuthProvider(environmentConfig) } @Provides @Singleton - fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider { - return DefaultP2PEthPoolAuthProvider(environmentConfigStorage) + fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider { + return DefaultP2PEthPoolAuthProvider(environmentConfig) } @Provides diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt index 82aa59fe63..4075e7dd57 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt @@ -21,6 +21,4 @@ interface SellService { walletAddress: String, isDarkTheme: Boolean, ): String? - - fun getSellCryptoReceiptUrl(transactionId: String): String? } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 60491ab4c7..742e44ec5e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency -import com.tangem.utils.Provider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import timber.log.Timber @@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec class MoonPayService( private val api: MoonPayApi, - private val apiKeyProvider: Provider, - private val secretKeyProvider: Provider, + private val apiKey: String, + private val secretKey: String, private val userWalletProvider: () -> UserWallet?, ) : SellService { @@ -47,18 +46,18 @@ class MoonPayService( _initializationStatus.value = lceLoading() performRequest { - val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) { + val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load user status", result.error) + Timber.e(result.error, "Failed to load user status") _initializationStatus.value = result.error.lceError() return@performRequest } is Result.Success -> result.data } - val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) { + val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load currencies", result.error) + Timber.e(result.error, "Failed to load currencies") _initializationStatus.value = result.error.lceError() return@performRequest } @@ -163,7 +162,7 @@ class MoonPayService( val uri = Uri.Builder() .scheme(SCHEME) .authority(URL_SELL) - .appendQueryParameter("apiKey", apiKeyProvider()) + .appendQueryParameter("apiKey", apiKey) .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}") @@ -177,17 +176,9 @@ class MoonPayService( return uri.build().toString() } - override fun getSellCryptoReceiptUrl(transactionId: String): String { - return Uri.Builder() - .scheme(SCHEME) - .authority(URL_SELL) - .appendPath("transaction_receipt") - .appendQueryParameter("transactionId", transactionId).build().toString() - } - private fun createSignature(data: String): String { val sha256Hmac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256") + val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") sha256Hmac.init(secretKey) val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray()) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) 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 133c325afc..d86379f650 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 @@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore @@ -63,7 +62,6 @@ data class DaggerGraphState( val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val environmentConfigStorage: EnvironmentConfigStorage? = null, val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, 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 aa9b5efd77..4131a1c99f 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 @@ -2,7 +2,6 @@ package com.tangem.tap.routing.utils import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.models.PortfolioId import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.referral.api.ReferralComponent @@ -18,7 +17,6 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryRoute -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent @@ -26,7 +24,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource -import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.nft.component.NFTComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent @@ -67,7 +64,6 @@ internal class ChildFactory @Inject constructor( private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, - private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen, private val onrampComponentFactory: OnrampComponent.Factory, private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory, @@ -117,7 +113,6 @@ internal class ChildFactory @Inject constructor( private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -147,11 +142,7 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT } - val mode = when (val portfolio = route.portfolioId) { - is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId) - is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId) - null -> ManageTokensMode.None - } + val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None createComponentChild( context = context, @@ -193,39 +184,21 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.MarketsTokenDetails -> { - if (feedFeatureToggle.isFeedEnabled) { - createComponentChild( - context = context, - params = FeedEntryRoute.MarketTokenDetails( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - FeedEntryRoute.MarketTokenDetails.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = feedEntryComponentFactory, - ) - } else { - createComponentChild( - context = context, - params = MarketsTokenDetailsComponent.Params( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = marketsTokenDetailsComponentFactory, - ) - } + createComponentChild( + context = context, + params = FeedEntryRoute.MarketTokenDetails( + token = route.token, + appCurrency = route.appCurrency, + shouldShowPortfolio = route.shouldShowPortfolio, + analyticsParams = route.analyticsParams?.let { params -> + FeedEntryRoute.MarketTokenDetails.AnalyticsParams( + blockchain = params.blockchain, + source = params.source, + ) + }, + ), + componentFactory = feedEntryComponentFactory, + ) } is AppRoute.Onramp -> { createComponentChild( diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 27c065e869..4bc37fc48e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor( private val promoDeepLink: PromoDeeplinkHandler.Factory, private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor( onboardVisaDeepLink.create(deeplinkUri) return } - deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> { + deeplinkUri.path?.startsWith("/news") == true -> { newsDetailsDeepLink.create(coroutineScope, deeplinkUri) return } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 3b1aea3367..ad92c990d1 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,21 +1,4 @@ - - - - - - - + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground_base.xml b/app/src/main/res/drawable/ic_launcher_foreground_base.xml new file mode 100644 index 0000000000..3b1aea3367 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground_base.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 8b20aae928..8cc7aedc0a 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/mocked/res/drawable/ic_launcher_foreground.xml b/app/src/mocked/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..ba2ae549e5 --- /dev/null +++ b/app/src/mocked/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt new file mode 100644 index 0000000000..16ba7e686f --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.data + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultOfframpRepositoryTest { + + private val sellService: SellService = mockk() + private val repository = DefaultOfframpRepository(sellService) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val fiatCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + + @BeforeEach + fun setUp() { + mockkObject(MutableAppThemeModeHolder) + } + + @AfterEach + fun tearDown() { + clearMocks(sellService) + unmockkObject(MutableAppThemeModeHolder) + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with light theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with dark theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns true + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } + } + + @Test + fun `getOfframpUrl should return null when sellService returns null`() { + // Arrange + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns null + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isNull() + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } +} diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt new file mode 100644 index 0000000000..5be7a5b371 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt @@ -0,0 +1,252 @@ +package com.tangem.tap.domain.tasks.product + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +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.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +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) +class BlockchainToDeriveFinderTest { + + private val walletAccountsFetcher = mockk() + private val finder = BlockchainToDeriveFinder( + walletAccountsFetcher = walletAccountsFetcher, + ) + + @AfterEach + fun tearDown() { + clearMocks(walletAccountsFetcher) + } + + @Test + fun `GIVEN card is not HD wallet THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns false + } + + // Act + val actual = finder.find(card) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN card has empty wallets THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.wallets } returns emptyList() + } + + // Act + val actual = finder.find(card) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest { + // Arrange + val card = createCardDTO() + + val response = createResponse(Blockchain.Bitcoin) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "AC01000000045754" + val card = createCardDTO(cardId = demoCardId) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + createExpected(Blockchain.Solana), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "DE00" + val card = createCardDTO(cardId = demoCardId) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store THEN return default blockchains`() = runTest { + // Arrange + val card = createCardDTO() + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved cardano THEN return only cardano`() = runTest { + // Arrange + val card = createCardDTO() + + val response = createResponse(Blockchain.Cardano) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Cardano), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest { + // Arrange + val card = createCardDTO() + + val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon) + + val response = createResponse(*blockchains.toTypedArray()) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = blockchains.mapTo(hashSetOf(), ::createExpected) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO { + val wallet = mockk { + every { this@mockk.publicKey } returns byteArrayOf(0) + } + + return mockk { + every { this@mockk.cardId } returns cardId + every { this@mockk.batchId } returns batchId + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.settings.isKeysImportAllowed } returns true + every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release, + ) + every { this@mockk.wallets } returns listOf(wallet) + } + } + + private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse { + val tokens = blockchains.map { blockchain -> + mockk { + every { this@mockk.networkId } returns blockchain.toNetworkId() + every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath + every { this@mockk.contractAddress } returns null + } + } + + val account = mockk { + every { this@mockk.tokens } returns tokens + } + + return mockk { + every { this@mockk.accounts } returns listOf(account) + } + } + + private fun createExpected( + blockchain: Blockchain, + derivationPath: DerivationPath = blockchain.getDerivationPath(), + ): BlockchainToDerive { + return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath) + } + + private fun Blockchain.getDerivationPath(): DerivationPath { + return derivationPath(DerivationStyle.V3)!! + } + + private companion object { + + // for byteArrayOf(0) + val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") + } +} diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 19782fca91..4d40266dc8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -87,7 +86,6 @@ class DeepLinkFactoryTest { private val newsDeeplink = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } - private val feedFeatureToggle = mockk() private val mockedUri = mockk(relaxed = true) private val isFromOnNewIntent: Boolean = false @@ -112,7 +110,6 @@ class DeepLinkFactoryTest { promoDeepLink = promoDeepLinkFactory, onboardVisaDeepLink = onboardVisaDeepLink, newsDetailsDeepLink = newsDeeplink, - feedFeatureToggle = feedFeatureToggle, ) @OptIn(ExperimentalCoroutinesApi::class) diff --git a/build.gradle.kts b/build.gradle.kts index c8864ff496..b61f203435 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,3 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import java.util.concurrent.ConcurrentHashMap - plugins { alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.jvm) apply false @@ -33,83 +30,13 @@ interface Injected { val fs: FileSystemOperations } -data class TestStats( - val total: Long = 0, - val passed: Long = 0, - val failed: Long = 0, - val skipped: Long = 0, -) - -val testResultsByModule = ConcurrentHashMap() - // Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules val unitTest by tasks.registering { group = "verification" description = "Run unit tests for debug/googleDebug variant and all JVM modules" - - doLast { - if (testResultsByModule.isNotEmpty()) { - val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats -> - TestStats( - total = acc.total + stats.total, - passed = acc.passed + stats.passed, - failed = acc.failed + stats.failed, - skipped = acc.skipped + stats.skipped, - ) - } - - println("\n" + "=".repeat(80)) - println("TEST SUMMARY") - println("=".repeat(80)) - - testResultsByModule.toSortedMap().forEach { (module, stats) -> - println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)") - } - - println("-".repeat(80)) - println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules") - println(" Passed: ${totalStats.passed}") - println(" Failed: ${totalStats.failed}") - println(" Skipped: ${totalStats.skipped}") - println("=".repeat(80)) - } - } } -// Test Logging and testCI dependencies subprojects { - tasks.withType().configureEach { - println("Test task scheduled: $path") - - testLogging { - exceptionFormat = TestExceptionFormat.FULL - showStandardStreams = true - - afterSuite(KotlinClosure2({ desc, result -> - if (desc.parent == null) { // will match the outermost suite - testResultsByModule[path] = TestStats( - total = result.testCount, - passed = result.successfulTestCount, - failed = result.failedTestCount, - skipped = result.skippedTestCount, - ) - - val output = - "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" - val startItem = "| " - val endItem = " |" - val repeatLength = startItem.length + output.length + endItem.length - println( - "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( - repeatLength - ) - ) - } - })) - } - } - - // Register testCI dependencies // App module plugins.withId("com.android.application") { afterEvaluate { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index a514a89100..07b256a7eb 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -7,13 +7,12 @@ import android.os.Bundle import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.serialization.SerializedBigDecimal @@ -21,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.model.details.NavigationAction import kotlinx.serialization.Serializable @@ -128,8 +128,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( val source: Source, - val portfolioId: PortfolioId? = null, - ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { + val accountId: AccountId? = null, + ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${accountId?.value}") { /** * Source of launching the screen. @@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class EditAccount( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/edit_account/${account.accountId.value}") @Serializable data class AccountDetails( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/account_details/${account.accountId.value}") @Serializable diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt index 64554a30e0..1830c50c92 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt @@ -1,31 +1,11 @@ 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" + return "$BASE_URL/$CRYPTOCURRENCIES_PATH/$tokenId" } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt index 6dc7f0d926..ab45c7159d 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine object TangemSiteUrlBuilder { + const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10" + suspend fun getUtmTags(campaign: String?): String { val langCode = Locale.getDefault().language val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() diff --git a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt index a4a646a6ab..973f6c6895 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt @@ -73,7 +73,7 @@ object MockScanResponseFactory { CardDTO.Wallet( CardWallet( publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name - chainCode = null, + chainCode = ByteArray(32), // chainCode must not be null for HD wallets curve = curve, settings = createSettings(), totalSignedHashes = null, diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt index 943fb98735..1955e4b9a0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt @@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter( ) return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(this), + iconState = AccountIconItemStateConverter().convert(this), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content { return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 102be5a837..6cc4b2bf7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -1,11 +1,14 @@ package com.tangem.common.ui.account +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.utils.converter.Converter -object AccountIconItemStateConverter : Converter { +class AccountIconItemStateConverter( + val size: AccountIconSize = AccountIconSize.Default, +) : Converter { override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { is Account.CryptoPortfolio -> when { @@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter CurrencyIconState.CryptoPortfolio.Icon( resId = value.icon.value.getResId(), color = value.icon.color.getUiColor(), isGrayscale = false, + size = size, ) } is Account.Payment -> TODO("[REDACTED_JIRA]") diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt new file mode 100644 index 0000000000..f4d414cae3 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.common.ui.account + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList + +class TokensListPortfolioItemConverter( + val tokenItemUM: TokenItemState, + val isExpanded: Boolean, + val isCollapsable: Boolean, + val tokens: ImmutableList, + val onEmptyAction: PortfolioItemContentUM.Empty.Action? = null, +) : Converter { + + override fun convert(value: Unit): TokensListItemUM.Portfolio { + val content = if (tokens.isEmpty()) { + PortfolioItemContentUM.Empty(onEmptyAction) + } else { + PortfolioItemContentUM.Tokens(tokens) + } + return TokensListItemUM.Portfolio( + tokenItemUM = tokenItemUM, + isExpanded = isExpanded, + isCollapsable = isCollapsable, + content = content, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt deleted file mode 100644 index 89855cf6ce..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.common.ui.alerts - -import com.tangem.common.ui.alerts.models.AlertDemoModeUM -import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.utils.converter.Converter - -class TransactionErrorAlertConverter( - private val popBackStack: () -> Unit, - private val onFailedTxEmailClick: (String) -> Unit, -) : Converter { - override fun convert(value: SendTransactionError): AlertUM? { - return when (value) { - is SendTransactionError.DemoCardError -> AlertDemoModeUM( - onConfirmClick = popBackStack, - ) - is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = null, - causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), - onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, - ) - is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = value.message, - onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, - ) - is SendTransactionError.DataError -> AlertTransactionErrorUM( - code = "", - cause = value.message, - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.NetworkError -> AlertTransactionErrorUM( - code = value.code.orEmpty(), - cause = value.message.orEmpty(), - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.UnknownError -> AlertTransactionErrorUM( - code = "", - cause = value.ex?.localizedMessage, - onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, - ) - is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM( - code = "", - cause = null, - causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)), - onConfirmClick = popBackStack, - ) - else -> null - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt new file mode 100644 index 0000000000..c5476fa263 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.transaction.error.SendTransactionError +import javax.inject.Inject + +class TransactionErrorDialogFactory @Inject constructor() { + + fun create( + error: SendTransactionError, + popBackStack: () -> Unit, + onFailedTxEmailClick: (String) -> Unit, + ): DialogMessage? { + return when (error) { + is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack) + is SendTransactionError.TangemSdkError -> transactionErrorDialog( + causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)), + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick(error.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> transactionErrorDialog( + cause = error.message, + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> transactionErrorDialog( + cause = error.message, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> transactionErrorDialog( + cause = error.message.orEmpty(), + code = error.code.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> transactionErrorDialog( + cause = error.ex?.localizedMessage, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) }, + ) + is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog( + causeTextReference = resourceReference( + R.string.no_account_polkadot, + wrappedList(error.amount), + ), + code = "", + onConfirmClick = popBackStack, + ) + else -> null + } + } + + private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) + + private fun transactionErrorDialog( + cause: String? = null, + causeTextReference: TextReference? = null, + code: String, + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.send_alert_transaction_failed_title), + message = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt deleted file mode 100644 index e63ccb0229..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference - -data class AlertDemoModeUM( - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) - override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt deleted file mode 100644 index 6d9cea587a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList - -data class AlertTransactionErrorUM( - val code: String, - val cause: String?, - val causeTextReference: TextReference? = null, - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) - override val message: TextReference = resourceReference( - id = R.string.send_alert_transaction_failed_text, - formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt deleted file mode 100644 index 1cf955bebe..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference - -@Immutable -interface AlertUM { - val title: TextReference? - val message: TextReference - val confirmButtonText: TextReference - val onConfirmClick: (() -> Unit)? -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 4b4ca2eb11..7af3019d50 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList +@Deprecated("Use GiveApprovalComponent") @Composable fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { var isPermissionAlertShow by remember { mutableStateOf(false) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt deleted file mode 100644 index 15bac87bdc..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.common.ui.news - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -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.components.label.Label -import com.tangem.core.ui.components.label.entity.LabelUM -import com.tangem.core.ui.res.TangemTheme -import kotlinx.collections.immutable.ImmutableList - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ArticleHeader( - title: String, - createdAt: String, - score: Float, - tags: ImmutableList, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - ArticleInfo( - score = score, - createdAt = createdAt, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = title, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - - if (tags.isNotEmpty()) { - Spacer(modifier = Modifier.height(20.dp)) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - tags.forEach { tag -> - Label( - state = tag, - ) - } - } - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt deleted file mode 100644 index 300d41f104..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.common.ui.news - -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.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 - -@Composable -fun TrendingLoadingArticle(modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp, horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp) - SpacerH(12.dp) - RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp) - SpacerH(6.dp) - RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp) - SpacerH(14.dp) - RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp) - SpacerH(32.dp) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) - RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) - RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) - } - } - } -} - -@Composable -fun DefaultLoadingArticle(modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) { - Column(modifier = Modifier.padding(12.dp)) { - RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp) - SpacerH(12.dp) - RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp) - SpacerH(6.dp) - RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp) - SpacerH(6.dp) - RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp) - SpacerH(16.dp) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) - RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) - } - } - } -} \ No newline at end of file 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 7f8ec96750..161788fcb3 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 @@ -1,17 +1,35 @@ package com.tangem.common.ui.notifications +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize 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.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.TangemPagerIndicator import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf fun LazyListScope.notifications( notifications: ImmutableList, @@ -106,8 +124,107 @@ fun LazyListScope.notifications( contentColor = contentColor, modifier = modifier .padding(top = topPadding) - .animateItem(), + .animateItem(null, null, null), ) }, ) -} \ No newline at end of file +} + +/** + * Displays a list of notifications in a stacked manner using a HorizontalPager. + * If there are multiple notifications, a PagerIndicator is shown below the notifications. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param containerColor Color to be used for the background of the notifications. + * @param modifier Optional Modifier for the notifications. + */ +fun LazyListScope.notificationsCarousel( + notifications: ImmutableList?, + containerColor: Color, + modifier: Modifier = Modifier, +) { + item { + if (!notifications.isNullOrEmpty()) { + val notificationsPagerState = rememberPagerState( + pageCount = { notifications.size }, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + HorizontalPager( + state = notificationsPagerState, + modifier = Modifier + .fillMaxSize() + .animateItem(null, null, null), + ) { page -> + TangemMessage( + messageUM = notifications[page], + contentColor = containerColor, + modifier = modifier, + ) + } + if (notifications.size > 1) { + TangemPagerIndicator( + pagerState = notificationsPagerState, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun StackedNotifications_Preview( + @PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList, +) { + TangemThemePreviewRedesign { + val contentColor = TangemTheme.colors2.surface.level1 + LazyColumn( + modifier = Modifier + .background(contentColor) + .padding(16.dp), + ) { + notificationsCarousel( + notifications = params, + containerColor = contentColor, + ) + } + } +} + +private class StackedNotificationsPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + ), + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + TangemMessageUM( + id = "1", + title = stringReference("Second notification"), + subtitle = stringReference("This is the second notification"), + messageEffect = TangemMessageEffect.Card, + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt new file mode 100644 index 0000000000..0a7afcbb8d --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.userwallet.converter + +import androidx.compose.ui.graphics.Color +import androidx.core.graphics.toColorInt +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.domain.models.wallet.UserWalletIcon +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +/** + * Converter for mapping [UserWalletIcon] to [DeviceIconUM], + * which is used for displaying the wallet icon in the UI. + */ +class WalletIconUMConverter @Inject constructor() : Converter { + + override fun convert(value: UserWalletIcon): DeviceIconUM = with(value) { + fun String.parseHexColor(): Color = try { + Color(this.toColorInt()) + } catch (_: IllegalArgumentException) { + Color.Unspecified + } + + return when (this) { + UserWalletIcon.Hot -> DeviceIconUM.Mobile + is UserWalletIcon.Stub -> + DeviceIconUM.Stub(cardsCount = this.cardsCount) + is UserWalletIcon.Default -> if (isRing) { + DeviceIconUM.Ring( + mainColor = Color.Unspecified, + cardColor = Color.Unspecified, + secondCardColor = if (cardsCount > 2) Color.Unspecified else null, + ) + } else { + DeviceIconUM.Card( + mainColor = Color.Unspecified, + secondColor = if (cardsCount > 1) Color.Unspecified else null, + thirdColor = if (cardsCount > 2) Color.Unspecified else null, + ) + } + is UserWalletIcon.Colored -> if (isRing) { + DeviceIconUM.Ring( + mainColor = mainColor.parseHexColor(), + cardColor = secondColor?.parseHexColor(), + secondCardColor = thirdColor?.parseHexColor(), + ) + } else { + DeviceIconUM.Card( + mainColor = mainColor.parseHexColor(), + secondColor = secondColor?.parseHexColor(), + thirdColor = thirdColor?.parseHexColor(), + ) + } + } + } +} \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt index 3934f4ba5c..30c1e3b07e 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt @@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager import com.tangem.core.abtests.manager.impl.StubABTestsManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.utils.Provider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,7 +23,7 @@ internal object ABTestsManagerModule { @Singleton fun provideABTestsManager( application: Application, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchers: CoroutineDispatcherProvider, ): ABTestsManager { return if (BuildConfig.AB_TESTS_ENABLED) { @@ -32,7 +31,7 @@ internal object ABTestsManagerModule { } else { AmplitudeABTestsManager( application = application, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey }, + apiKey = environmentConfig.amplitudeApiKey, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ) } diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt index d2b6dbf52c..2307363d90 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -7,14 +7,13 @@ import com.amplitude.experiment.ExperimentConfig import com.amplitude.experiment.ExperimentUser import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.utils.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber internal class AmplitudeABTestsManager( val application: Application, - val apiKeyProvider: Provider, + val apiKey: String, val scope: CoroutineScope, ) : ABTestsManager { @@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager( client = Experiment.initializeWithAmplitudeAnalytics( application = application, - apiKey = apiKeyProvider(), + apiKey = apiKey, config = ExperimentConfig .builder() .automaticFetchOnAmplitudeIdentityChange(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 ae0d6a4253..946ad653c4 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 @@ -98,6 +98,7 @@ sealed class AnalyticsParam { data object NewsLink : ScreensSources("News Link") data object NewsPage : ScreensSources("News Page") data object Portfolio : ScreensSources("Portfolio") + data object Staking : ScreensSources("Staking") } sealed class TxSentFrom(val value: String) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt new file mode 100644 index 0000000000..e0271457e7 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent + +/** + * Offramp (withdraw/sell) analytics events + */ +sealed class OfframpAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) { + + /** + * Withdraw screen opened event + */ + data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened") +} \ 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 c4ccee2282..7399fee331 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 @@ -7,14 +7,7 @@ "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" }, - { - "name": "STAKING_TON_ENABLED", - "version": "5.28.0" - }, - { - "name": "STAKING_CARDANO_ENABLED", - "version": "5.31.1" - }, + { "name": "STAKING_ETH_ENABLED", "version": "undefined" @@ -31,52 +24,40 @@ "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "TANGEM_PAY_ENABLED", - "version": "5.31.0" - }, - { - "name": "YIELD_SUPPLY_FEATURE_ENABLED", - "version": "5.30.0" - }, - { - "name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - "version": "5.33.0" - }, - { - "name": "NEW_ONRAMP_MAIN_ENABLED", - "version": "5.31.0" - }, - { - "name": "ACCOUNTS_FEATURE_ENABLED", - "version": "5.33.0" - }, - { - "name": "FEED_ENABLED", - "version": "5.33.0" - }, { "name": "APP_REDESIGN_ENABLED", "version": "undefined" }, - { - "name": "GASLESS_TRANSACTIONS_ENABLED", - "version": "5.33.0" - }, { "name": "SWAP_MARKET_LIST_ENABLED", "version": "5.34" }, { "name": "EARN_BLOCK_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "HOLD_TO_CONFIRM_BUTTON_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "WALLET_REORDER_FEATURE_ENABLED", "version": "5.34" + }, + { + "name": "GASLESS_APPROVAL_ENABLED", + "version": "undefined" + }, + { + "name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED", + "version": "undefined" + }, + { + "name": "MULTI_ADDRESS_UTXO_ENABLED", + "version": "undefined" + }, + { + "name": "CUSTOMER_IO_ENABLED", + "version": "5.35" } ] diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 8df1d5a9f9..353cb3f5ab 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -1,4 +1,6 @@ +import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants +import com.tangem.plugin.configuration.model.BuildType plugins { alias(deps.plugins.android.library) @@ -10,6 +12,23 @@ plugins { id("configuration") } +abstract class GenerateEnvironmentConfigTask : DefaultTask() { + + @get:InputFile + abstract val configFile: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val input = configFile.get().asFile + require(input.exists()) { "Config file not found: ${input.absolutePath}" } + logger.lifecycle("Generating EnvironmentConfig from ${input.name}") + EnvironmentConfigGenerator.generate(input, outputDir.get().asFile) + } +} + android { namespace = "com.tangem.datasource" @@ -18,6 +37,27 @@ android { } } +androidComponents { + onVariants { variant -> + val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug + val configFile = rootProject.file( + "app/src/main/assets/tangem-app-config/config_${buildType.environment}.json", + ) + + val taskProvider = tasks.register( + "generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}", + ) { + this.configFile.set(configFile) + outputDir.set(layout.buildDirectory.dir("generated/source/environment-config/${variant.name}")) + doFirst { + logger.lifecycle("[Environment config] Running: ${this.name}") + } + } + + variant.sources.java?.addGeneratedSourceDirectory(taskProvider, GenerateEnvironmentConfigTask::outputDir) + } +} + tasks.withType().configureEach { useJUnitPlatform() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt index f05797c816..7fcda27661 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt @@ -1,11 +1,10 @@ package com.tangem.datasource.api.common.config -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend -import kotlinx.coroutines.flow.first internal class BlockAid( - private val configStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -21,9 +20,7 @@ internal class BlockAid( put( key = "X-API-KEY", value = ProviderSuspend { - requireNotNull( - configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey, - ) + requireNotNull(environmentConfig.blockAidApiKey) }, ) put("accept", ProviderSuspend { "application/json" }) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 4104dae025..b9923936b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.utils.ProviderSuspend @@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider /** * Express [ApiConfig] * - * @property environmentConfigStorage environment config storage + * @property environmentConfig environment config * @property expressAuthProvider express auth provider * @property appVersionProvider app version provider * @property appInfoProvider app info provider */ internal class Express( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val expressAuthProvider: ExpressAuthProvider, private val appVersionProvider: AppVersionProvider, private val appInfoProvider: AppInfoProvider, @@ -100,9 +100,9 @@ internal class Express( private fun getApiKey(isProd: Boolean): String { return if (isProd) { - environmentConfigStorage.getConfigSync().express + environmentConfig.express } else { - environmentConfigStorage.getConfigSync().devExpress + environmentConfig.devExpress } ?.apiKey ?: error("No express config provided") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index b494cbc70c..5c20eca2cf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider internal sealed class TangemPay( + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, - private val environmentConfigStorage: EnvironmentConfigStorage, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() @@ -61,8 +61,8 @@ internal sealed class TangemPay( return when (apiEnvironment) { ApiEnvironment.MOCK, ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().bffStaticTokenDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken + -> environmentConfig.bffStaticTokenDev + ApiEnvironment.PROD -> environmentConfig.bffStaticToken ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, ApiEnvironment.DEV_2, @@ -72,9 +72,9 @@ internal sealed class TangemPay( } class Bff( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/" @@ -90,9 +90,9 @@ internal sealed class TangemPay( } class Auth( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index b5e7ea38c6..ed04143824 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider @@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider /** YieldSupply [ApiConfig] */ internal class YieldSupply( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, private val authProvider: AuthProvider, private val appInfoProvider: AppInfoProvider, @@ -78,8 +78,8 @@ internal class YieldSupply( ApiEnvironment.DEV_3, ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, - -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey + -> environmentConfig.yieldModuleApiKeyDev + ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey } ?: error("No tangem tech api config provided") } } \ No newline at end of file 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 48cc8fc4fa..e8ce00ce4f 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 @@ -12,7 +12,7 @@ data class OrderResponse( @Json(name = "id") val id: String, @Json(name = "customer_id") val customerId: String?, @Json(name = "type") val type: String?, - @Json(name = "status") val status: String, + @Json(name = "status") val status: Status, @Json(name = "step") val step: String?, @Json(name = "data") val data: Data, @Json(name = "step_change_code") val stepChangeCode: Int?, @@ -29,5 +29,20 @@ data class OrderResponse( @Json(name = "payment_account_id") val paymentAccountId: String?, @Json(name = "transaction_hash") val transactionHash: String?, ) + + @JsonClass(generateAdapter = false) + enum class Status { + @Json(name = "NEW") + NEW, + + @Json(name = "PROCESSING") + PROCESSING, + + @Json(name = "COMPLETED") + COMPLETED, + + @Json(name = "CANCELED") + CANCELED, + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt index 6938d499d7..40ffd06873 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt @@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig internal class Sha256SignatureVerifier( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val apiConfigsManager: ApiConfigsManager, ) : DataSignatureVerifier { @@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier( private fun getPubKey(): String? { val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express) return when (expressConfig.environment) { - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey - else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey + ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey + else -> environmentConfig.devExpress?.signVerifierPublicKey } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 20d4c632d5..e8db8ec219 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -21,13 +21,13 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideExpressConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, expressAuthProvider: ExpressAuthProvider, appVersionProvider: AppVersionProvider, appInfoProvider: AppInfoProvider, ): ApiConfig { return Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -73,12 +73,12 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideYieldSupplyConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, authProvider: AuthProvider, appInfoProvider: AppInfoProvider, ): ApiConfig = YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = authProvider, appInfoProvider = appInfoProvider, @@ -87,21 +87,21 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideTangemPayBffConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider) @Provides @IntoSet fun provideTangemPayAuthConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider) @Provides @IntoSet - fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { - return BlockAid(environmentConfigStorage) + fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig { + return BlockAid(environmentConfig) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index c1e4a4f7dc..de63c7bbcd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -45,6 +46,15 @@ class MoshiModule { .withSubtype(NetworkStatusDM.Verified::class.java, "amounts") .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) + .add( + NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) + .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") + .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") + .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") .withSubtype(NFTCollection.Identifier.EVM::class.java, "evm") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt index 82d2ca69da..c04614c1f7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.Sha256SignatureVerifier -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,9 +17,9 @@ internal object SecurityModule { @Provides @Singleton fun provideDataSignatureVerifier( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, apiConfigsManager: ApiConfigsManager, ): DataSignatureVerifier { - return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager) + return Sha256SignatureVerifier(environmentConfig, apiConfigsManager) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt index 0ed4457201..c8052af564 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt @@ -1,9 +1,8 @@ package com.tangem.datasource.di.local.config import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage @@ -23,11 +22,8 @@ internal object ConfigModule { @Provides @Singleton - fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage { - return DefaultEnvironmentConfigStorage( - assetLoader = assetLoader, - environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()), - ) + fun provideEnvironmentConfig(): EnvironmentConfig { + return GeneratedEnvironmentConfigConverter.convert() } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt deleted file mode 100644 index 1ad363973d..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import com.tangem.datasource.BuildConfig -import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.datasource.local.datastore.RuntimeStateStore -import kotlinx.coroutines.flow.Flow -import timber.log.Timber - -/** - * Default implementation for storing [EnvironmentConfig] - * - * @property assetLoader asset loader - * @property environmentConfigStore config store - */ -internal class DefaultEnvironmentConfigStorage( - private val assetLoader: AssetLoader, - private val environmentConfigStore: RuntimeStateStore, -) : EnvironmentConfigStorage { - - override suspend fun initialize(): EnvironmentConfig { - val environmentConfigModel = assetLoader.load(fileName = CONFIG_FILE_NAME) - ?: return environmentConfigStore.get().value - - val config = EnvironmentConfigConverter.convert(value = environmentConfigModel) - environmentConfigStore.store(value = config) - - Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully") - - return config - } - - override fun getConfig(): Flow = environmentConfigStore.get() - - override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value - - private companion object { - const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}" - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 22545b5329..21e234d6f5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -28,4 +28,6 @@ data class EnvironmentConfig( val bffStaticTokenDev: String? = null, val gaslessTxApiKeyDev: String? = null, val gaslessTxApiKey: String? = null, + val customerIoCdpApiKey: String? = null, + val surveySparrowToken: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt deleted file mode 100644 index a46edc5d9a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import kotlinx.coroutines.flow.Flow - -/** - * Storage for [EnvironmentConfig] - * -[REDACTED_AUTHOR] - */ -interface EnvironmentConfigStorage { - - /** Initialize and return [EnvironmentConfig] */ - suspend fun initialize(): EnvironmentConfig - - /** Get [EnvironmentConfig] as [Flow] */ - fun getConfig(): Flow - - /** Get [EnvironmentConfig] synchronously */ - fun getConfigSync(): EnvironmentConfig -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt new file mode 100644 index 0000000000..28644f5c6b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt @@ -0,0 +1,183 @@ +package com.tangem.datasource.local.config.environment.converter + +import com.tangem.blockchain.common.* +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey +import com.tangem.datasource.local.config.environment.models.ExpressModel +import com.tangem.datasource.local.config.environment.models.P2PKeys + +/** + * Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig] + * + * This converter maps the auto-generated config (from JSON) to the domain model. + * The generated config has nested objects that mirror the JSON structure. + */ +internal object GeneratedEnvironmentConfigConverter { + + fun convert(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey, + moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey, + mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId, + mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret, + blockchainSdkConfig = createBlockchainSdkConfig(), + amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey, + appsFlyerApiKey = AppsFlyer.appsFlyerDevKey, + appsAppId = AppsFlyer.appsFlyerAppID, + walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId, + express = createExpressModel( + apiKey = Express.apiKey, + signVerifierPublicKey = Express.signVerifierPublicKey, + ), + devExpress = createExpressModel( + apiKey = DevExpress.apiKey, + signVerifierPublicKey = DevExpress.signVerifierPublicKey, + ), + stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey, + p2pApiKey = createP2PKeys(), + blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey, + tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey, + tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev, + tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage, + yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey, + yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev, + bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken, + bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev, + gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev, + gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey, + customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.androidApiKey, + surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey, + ) + } + + private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? { + return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) { + ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey) + } else { + null + } + } + + private fun createP2PKeys(): P2PKeys? { + val mainnet = P2pApiKey.mainnet + val hoodi = P2pApiKey.hoodi + return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) { + P2PKeys(mainnet = mainnet, hoodi = hoodi) + } else { + null + } + } + + private fun createBlockchainSdkConfig(): BlockchainSdkConfig { + return BlockchainSdkConfig( + blockchairCredentials = BlockchairCredentials( + apiKey = GeneratedEnvironmentConfig.blockchairApiKeys, + authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken, + ), + blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(), + quickNodeSolanaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain, + ), + quickNodeBscCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain, + ), + quickNodePlasmaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain, + ), + quickNodeMonadCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain, + ), + infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId, + tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey, + nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey), + getBlockCredentials = createGetBlockCredentials(), + kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl, + tonCenterCredentials = TonCenterCredentials( + mainnetApiKey = TonCenterApiKey.mainnet, + testnetApiKey = TonCenterApiKey.testnet, + ), + chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey, + chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey, + hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey, + polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey, + bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey, + bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey, + dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey, + koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey, + alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey, + moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey, + etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey, + blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey, + tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey, + ) + } + + private fun createGetBlockCredentials(): GetBlockCredentials { + return GetBlockCredentials( + xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc), + cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta), + avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc), + eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc), + etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc), + fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc), + rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc), + bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc), + polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc), + gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc), + cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc), + solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc), + ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc), + tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest), + cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest), + near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc), + aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest), + dogecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest, + ), + litecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest, + ), + dash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dash.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dash.blockBookRest, + ), + bitcoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest, + ), + algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest), + zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc), + polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc), + base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc), + blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc), + filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc), + arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc), + bitcoinCash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc, + blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest, + ), + kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc), + moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc), + optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc), + polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc), + shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc), + sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc), + telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc), + tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest), + monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest), + stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt new file mode 100644 index 0000000000..589fb4d915 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt @@ -0,0 +1,53 @@ +@file:Suppress("BooleanPropertyNaming") +package com.tangem.datasource.local.visa.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.domain.models.kyc.KycStatus +import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType +import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel +import java.math.BigDecimal + +/** + * Payment account status for storage in the local cache. + * + * @see [com.tangem.domain.pay.PaymentAccountStatus] + */ +@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) +sealed interface PaymentAccountStatusDM { + + @NameLabel("not_created") + data class NotCreated( + @Json(name = "not_created") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("kyc_status") + data class UnderReview( + @Json(name = "kyc_status") val kycStatus: KycStatus, + ) : PaymentAccountStatusDM + + @NameLabel("issuing_card") + data class IssuingCard( + @Json(name = "issuing_card") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("locked") + data class Locked( + @Json(name = "locked") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("balance") + data class Loaded( + @Json(name = "card_id") val cardId: String, + @Json(name = "last_four_digits") val lastFourDigits: String, + @Json(name = "balance") val balance: BigDecimal, + @Json(name = "currency_code") val currencyCode: String, + @Json(name = "deposit_address") val depositAddress: String?, + @Json(name = "is_pin_set") val isPinSet: Boolean, + ) : PaymentAccountStatusDM + + @NameLabel("card_issue_failed") + data class CardIssueFailed( + @Json(name = "card_issue_failed") val marker: Boolean = true, + ) : PaymentAccountStatusDM +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index 2e0ccb145b..5650f72e16 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config import com.google.common.truth.Truth import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import io.mockk.clearMocks import io.mockk.every @@ -19,6 +20,7 @@ class ApiConfigTest { private val appAuthProvider = mockk() private val apiKeyProvider = mockk>() + private val environmentConfig = mockk() @BeforeEach fun setup() { @@ -47,7 +49,7 @@ class ApiConfigTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, expressAuthProvider = mockk(), appVersionProvider = mockk(), appInfoProvider = mockk(), @@ -55,7 +57,7 @@ class ApiConfigTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, appVersionProvider = mockk(), authProvider = appAuthProvider, appInfoProvider = mockk(), @@ -70,14 +72,14 @@ class ApiConfigTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk()) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk()) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk()) ApiConfig.ID.News -> News( diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt deleted file mode 100644 index 530d4de06e..0000000000 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.datasource.api.common.config.managers - -import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.datasource.local.config.environment.models.ExpressModel -import kotlinx.coroutines.flow.flowOf - -/** - * Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest] - * -[REDACTED_AUTHOR] - */ -internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { - - private val environmentConfig = EnvironmentConfig( - express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"), - devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"), - blockAidApiKey = BLOCK_AID_API_KEY, - tangemApiKey = TANGEM_API_KEY, - tangemApiKeyDev = TANGEM_API_KEY_DEV, - bffStaticToken = TANGEM_PAY_BFF_KEY, - bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, - tangemApiKeyStage = TANGEM_API_KEY_STAGE, - yieldModuleApiKey = YIELD_MODULE_KEY, - yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV, - ) - - override suspend fun initialize() = environmentConfig - override fun getConfig() = flowOf(environmentConfig) - override fun getConfigSync() = environmentConfig - - companion object { - const val EXPRESS_API_KEY = "express_api_key" - const val EXPRESS_DEV_API_KEY = "express_dev_api_key" - const val BLOCK_AID_API_KEY = "block_aid_api_key" - const val TANGEM_API_KEY = "tangem_api_key" - const val TANGEM_API_KEY_DEV = "tangem_api_key_dev" - const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key" - const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" - const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" - const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage" - const val YIELD_MODULE_KEY = "yield_module_api_key" - const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev" - } -} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 76572d804f..7b324258f2 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.models.ExpressModel import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -39,7 +37,7 @@ import java.util.TimeZone @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ProdApiConfigsManagerTest { - private val environmentConfigStorage = MockEnvironmentConfigStorage() + private val environmentConfig = createMockEnvironmentConfig() private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() @@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = appAuthProvider, appInfoProvider = appInfoProvider, @@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider) ApiConfig.ID.News -> News( @@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest { headers = mapOf( "api-key" to ProviderSuspend { if (environment == ApiEnvironment.PROD) { - MockEnvironmentConfigStorage.EXPRESS_API_KEY + EXPRESS_API_KEY } else { - MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY + EXPRESS_DEV_API_KEY } }, "session-id" to ProviderSuspend { EXPRESS_SESSION_ID }, @@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest { environment = ApiEnvironment.PROD, baseUrl = "https://yield.tangem.org/", headers = mapOf( - "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY }, + "api-key" to ProviderSuspend { YIELD_MODULE_KEY }, "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, "version" to ProviderSuspend { VERSION_NAME }, @@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest { const val P2P_API_KEY = "p2p_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key" + + // Mock config values + const val TANGEM_API_KEY = "tangem_api_key" + const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" + const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" + const val BLOCK_AID_API_KEY = "block_aid_api_key" + const val EXPRESS_API_KEY = "express_api_key" + const val EXPRESS_DEV_API_KEY = "express_dev_api_key" + const val YIELD_MODULE_KEY = "yield_module_key" + + fun createMockEnvironmentConfig(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = "moon_pay_api_key", + moonPayApiSecretKey = "moon_pay_secret_key", + mercuryoWidgetId = "mercuryo_widget_id", + mercuryoSecret = "mercuryo_secret", + blockchainSdkConfig = mockk(relaxed = true), + amplitudeApiKey = "amplitude_api_key", + appsFlyerApiKey = "appsflyer_api_key", + appsAppId = "apps_app_id", + walletConnectProjectId = "wallet_connect_project_id", + express = ExpressModel( + apiKey = EXPRESS_API_KEY, + signVerifierPublicKey = "express_public_key", + ), + devExpress = ExpressModel( + apiKey = EXPRESS_DEV_API_KEY, + signVerifierPublicKey = "express_dev_public_key", + ), + stakeKitApiKey = STAKE_KIT_API_KEY, + p2pApiKey = null, + blockAidApiKey = BLOCK_AID_API_KEY, + tangemApiKey = TANGEM_API_KEY, + tangemApiKeyDev = TANGEM_API_KEY, + tangemApiKeyStage = TANGEM_API_KEY, + yieldModuleApiKey = YIELD_MODULE_KEY, + yieldModuleApiKeyDev = YIELD_MODULE_KEY, + bffStaticToken = TANGEM_PAY_BFF_KEY_DEV, + bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, + gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY, + gaslessTxApiKey = TANGEM_GASLESS_API_KEY, + ) + } } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f40c33cb35..b03f0df753 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -851,6 +851,7 @@ Related tokens Related news Stay in the loop + Trending score NFC is not available on your device About NFT NFT asset diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index db9bc30c60..f9b0a562a0 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) - implementation(deps.compose.reorderable) + api(deps.compose.reorderable) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) @@ -61,12 +61,12 @@ dependencies { api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) - implementation(deps.haze) { + api(deps.haze) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") } - implementation(deps.haze.materials) { + api(deps.haze.materials) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt deleted file mode 100644 index 1bdae671ae..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -/** - * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating - * elements and floating button at the bottom of the screen. - */ -@Composable -fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Box( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size100 + bottomBarHeight) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), - ) -} - -/** - * A composable that draws a fade effect. Used on screens with a list of repeating - * elements and floating button at the bottom of the screen. - */ -@Composable -fun Fade( - modifier: Modifier = Modifier, - backgroundColor: Color = TangemTheme.colors.background.secondary, - height: Dp = 32.dp, -) { - Box( - modifier = modifier - .fillMaxWidth() - .height(height) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), - ) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt new file mode 100644 index 0000000000..ea58b6415a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt @@ -0,0 +1,145 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.TangemTheme +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint + +/** + * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} + +/** + * A composable that draws a fade effect at the right end of the screen. Same as [BottomFade] + * but with a horizontal gradient. + */ +@Composable +fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + Box( + modifier = modifier + .fillMaxHeight() + .background( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} + +/** + * A composable that draws a fade effect at the bottom of the screen. Same as [BottomFade] + * but with a vertical blur. + */ +@Composable +fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .hazeEffectTangem( + style = HazeStyle( + blurRadius = 20.dp, + tint = HazeTint( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + backgroundColor = Color.Transparent, + ), + ) { + progressive = + HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f) + }, + ) +} + +/** + * A composable that draws a fade effect at the right end of the screen. Same as [HorizontalFade] + * but with blur. + */ +@Composable +fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxHeight() + .hazeEffectTangem( + style = HazeStyle( + blurRadius = 20.dp, + tint = HazeTint( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + backgroundColor = Color.Transparent, + ), + ) { + progressive = + HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f) + }, + ) +} + +/** + * A composable that draws a fade effect. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun Fade( + modifier: Modifier = Modifier, + backgroundColor: Color = TangemTheme.colors.background.secondary, + height: Dp = 32.dp, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(height) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt index 19f8e5fcbd..f3275d7024 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt @@ -8,16 +8,29 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + UnableToLoadDataV2(onRetryClick, modifier) + } else { + UnableToLoadDataV1(onRetryClick, modifier) + } +} + +@Composable +private fun UnableToLoadDataV1(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -37,11 +50,43 @@ fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { } } +@Composable +private fun UnableToLoadDataV2(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.markets_loading_error_title), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.secondary, + ) + TangemButton( + buttonUM = TangemButtonUM( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = onRetryClick, + type = TangemButtonType.Secondary, + size = TangemButtonSize.X8, + shape = TangemButtonShape.Rounded, + ), + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true) +@Composable +private fun PreviewV2() { + TangemThemePreviewRedesign { + UnableToLoadDataV2(onRetryClick = {}) + } +} + @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun PreviewV1() { TangemThemePreview { - UnableToLoadData(onRetryClick = {}) + UnableToLoadDataV1(onRetryClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index f6f4dd18c0..32bf5c0667 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction @@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AppBarWithSearchTestTags /** * App bar with close icon and search functionality @@ -135,7 +137,8 @@ private fun CollapsedSearchView( contentDescription = null, modifier = Modifier .clickable { onExpandedChange(true) } - .padding(end = TangemTheme.dimens.spacing16), + .padding(end = TangemTheme.dimens.spacing16) + .testTag(AppBarWithSearchTestTags.SEARCH_ICON), ) } } @@ -210,7 +213,8 @@ private fun ExpandedSearchView( modifier = Modifier .fillMaxWidth() .focusRequester(textFieldFocusRequester) - .onFocusChanged { onFocusChange(it.hasFocus) }, + .onFocusChanged { onFocusChange(it.hasFocus) } + .testTag(AppBarWithSearchTestTags.TEXT_FIELD), placeholder = { Text(text = placeholderSearchText) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt new file mode 100644 index 0000000000..4b7090b987 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt @@ -0,0 +1,70 @@ +@file:Suppress("MagicNumber", "UnnecessaryParentheses") +package com.tangem.core.ui.components.background + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import com.tangem.core.ui.shader.TangemShader +import com.tangem.core.ui.shader.runtime.buildEffect +import kotlin.math.round + +@Composable +fun Modifier.shaderBackground( + shader: TangemShader, + speed: Float = 1f, + fallback: () -> Brush = { + Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent)) + }, +): Modifier { + val runtimeEffect = remember(shader) { buildEffect(shader) } + var size: Size by remember { mutableStateOf(Size(-1f, -1f)) } + val speedModifier = shader.speedModifier + + val time by if (runtimeEffect.isSupported) { + var startMillis = remember(shader) { -1L } + produceState(0f, speedModifier) { + while (true) { + withInfiniteAnimationFrameMillis { frameTimeMillis -> + if (startMillis < 0) startMillis = frameTimeMillis + value = ((frameTimeMillis - startMillis) / 16.6f) / 10f + } + } + } + } else { + remember { mutableFloatStateOf(-1f) } + } + + return this then Modifier.onGloballyPositioned { + size = Size(it.size.width.toFloat(), it.size.height.toFloat()) + }.drawBehind { + runtimeEffect.update( + shader = shader, + time = (time * speed * speedModifier).round(3), + width = size.width, + height = size.height, + ) // set uniforms for the shaders + + if (runtimeEffect.isReady) { + drawRect(brush = runtimeEffect.build()) + } else { + drawRect(brush = fallback()) + } + } +} + +private fun Float.round(decimals: Int): Float { + var multiplier = 1.0f + repeat(decimals) { multiplier *= 10 } + return round(this * multiplier) / multiplier +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt new file mode 100644 index 0000000000..f4e9988a2b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt @@ -0,0 +1,169 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.components.background.northernlights + +import androidx.compose.runtime.Composable +import android.graphics.BlurMaskFilter +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas + +@Suppress("LongMethod") +@Composable +internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradient") + + // ── Circle 1 (left) ────────────────────────────────────────────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF3355EE), + targetValue = Color(0xFF5577FF), + animationSpec = infiniteRepeatable( + animation = tween(4_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "color1", + ) + val x1 by transition.animateFloat( + initialValue = 0.05f, + targetValue = 0.28f, + animationSpec = infiniteRepeatable( + animation = tween(5_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x1", + ) + val y1 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.18f, + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "y1", + ) + + // ── Circle 2 (right) ───────────────────────────────────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF7733CC), + targetValue = Color(0xFF4455EE), + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_500), + ), + label = "color2", + ) + val x2 by transition.animateFloat( + initialValue = 0.68f, + targetValue = 0.92f, + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x2", + ) + val y2 by transition.animateFloat( + initialValue = 0.02f, + targetValue = 0.20f, + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_000), + ), + label = "y2", + ) + + // ── Oval (center) ──────────────────────────────────────────────────────── + val ovalColor by transition.animateColor( + initialValue = Color(0xFF5533CC), + targetValue = Color(0xFF8844EE), + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_500), + ), + label = "ovalColor", + ) + // ── Circle 3 (center) ──────────────────────────────────────────────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF9933BB), + targetValue = Color(0xFFBB44DD), + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(3_000), + ), + label = "color3", + ) + val x3 by transition.animateFloat( + initialValue = 0.35f, + targetValue = 0.58f, + animationSpec = infiniteRepeatable( + animation = tween(6_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_000), + ), + label = "x3", + ) + val y3 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.15f, + animationSpec = infiniteRepeatable( + animation = tween(4_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(500), + ), + label = "y3", + ) + + var blurRadiusState by remember { mutableFloatStateOf(0f) } + val circlePaint1 = remember { Paint() } + val circlePaint2 = remember { Paint() } + val circlePaint3 = remember { Paint() } + val ovalPaint = remember { Paint() } + + Canvas(modifier = modifier) { + val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f) + val circleRadius = size.width * 0.52f + + // Update maskFilter only when blur radius changes meaningfully + if (blurRadiusState != blurRadius) { + blurRadiusState = blurRadius + val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) + circlePaint1.asFrameworkPaint().maskFilter = mf + circlePaint2.asFrameworkPaint().maskFilter = mf + circlePaint3.asFrameworkPaint().maskFilter = mf + ovalPaint.asFrameworkPaint().maskFilter = mf + } + + circlePaint1.color = color1.copy(alpha = 0.85f) + circlePaint2.color = color2.copy(alpha = 0.85f) + circlePaint3.color = color3.copy(alpha = 0.85f) + ovalPaint.color = ovalColor.copy(alpha = 0.80f) + + drawIntoCanvas { canvas -> + canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1) + canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2) + canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3) + + val halfW = size.width * 0.68f + val halfH = size.width * 0.24f + val ovalCx = size.width * 0.50f + val ovalCy = 0f + + canvas.drawOval( + Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH), + ovalPaint, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt new file mode 100644 index 0000000000..27b3583edc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt @@ -0,0 +1,148 @@ +@file:Suppress("MagicNumber") + +package com.tangem.core.ui.components.background.northernlights + +import android.os.Build +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.background.shaderBackground +import com.tangem.core.ui.res.LocalPowerSavingState +import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader + +/** + * Animated northern lights background. + * Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode. + */ +@Composable +fun NorthernLightsBackground( + containerColor: Color, + modifier: Modifier = Modifier, + forceSimpleVersion: Boolean = false, +) { + val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() + if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) { + NorthernLightsBackgroundWithShader(containerColor, modifier) + } else { + MovingColorfulBlubsBackground(modifier) + } +} + +@Suppress("LongMethod") +@Composable +private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2") + + // Each track cycles through 4 states (matching the screenshot frames): + // deep/dark → saturated+bright → light/pastel → vibrant/vivid → back + // 16 s total per track, staggered so no two tracks peak simultaneously. + + // ── Color 1 – indigo → bright blue → lavender → hot violet ────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF2A1480), + targetValue = Color(0xFF2A1480), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF2A1480) at 0 using FastOutSlowInEasing + Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing + Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing + Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + ), + label = "color1", + ) + + // ── Color 2 – dark blue → cyan-blue → sky → teal ───────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF1444AA), + targetValue = Color(0xFF1444AA), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF1444AA) at 0 using FastOutSlowInEasing + Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing + Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing + Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(4_000), + ), + label = "color2", + ) + + // ── Color 3 – dark purple → medium purple → rose pink → magenta ────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF4422BB), + targetValue = Color(0xFF4422BB), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF4422BB) at 0 using FastOutSlowInEasing + Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing + Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing + Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(8_000), + ), + label = "color3", + ) + + // ── Color 4 – dark violet → medium violet → light pink → hot pink ──────── + val color4 by transition.animateColor( + initialValue = Color(0xFF331199), + targetValue = Color(0xFF331199), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF331199) at 0 using FastOutSlowInEasing + Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing + Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing + Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(2_000), + ), + label = "color4", + ) + + // Keep a stable shader instance so the RuntimeShader is never recreated. + // Colors are pushed each recomposition via updateColors(). + val shader = remember { + NorthernLightsMeshGradientShader( + colors = arrayOf( + Color(0xFF2A1480), + Color(0xFF1444AA), + Color(0xFF4422BB), + Color(0xFF331199), + containerColor, + ), + speed = 0.5f, + scale = 4f, + ) + } + val colorsArray = remember { Array(5) { Color.Unspecified } } + colorsArray[0] = color1 + colorsArray[1] = color2 + colorsArray[2] = color3 + colorsArray[3] = color4 + colorsArray[4] = containerColor + shader.updateColors(colorsArray) + + Box( + modifier = modifier + .background(containerColor) + .fillMaxSize() + .shaderBackground(shader), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt index fb14a4a72e..fa8da4d095 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt @@ -5,13 +5,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.material3.ripple diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt index 386f268756..19e1491cf4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview fun TangemPullToRefreshContainer( config: PullToRefreshConfig, modifier: Modifier = Modifier, + indicatorModifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val state = rememberPullToRefreshState() @@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer( modifier = modifier, indicator = { Indicator( - modifier = Modifier.align(Alignment.TopCenter), + modifier = indicatorModifier.align(Alignment.TopCenter), isRefreshing = config.isRefreshing, state = state, containerColor = TangemTheme.colors.background.tertiary, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt index fa6f78a262..899a888c8a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt @@ -79,7 +79,7 @@ private fun DropdownMenuContent( content: @Composable ColumnScope.() -> Unit, ) { // Menu open/close animation. - val transition = updateTransition(expandedStates, "DropDownMenu") + val transition = rememberTransition(expandedStates, "DropDownMenu") val scale by transition.animateFloat( transitionSpec = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt index d34949ea34..b80af29a35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt @@ -32,15 +32,15 @@ internal fun ProvideHaze(content: @Composable () -> Unit) { */ @Composable fun Modifier.hazeEffectTangem( + state: HazeState = LocalHazeState.current, style: HazeStyle = HazeStyle.Unspecified, configure: HazeEffectScope.() -> Unit = {}, ): Modifier { val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() - val hazeState = LocalHazeState.current - val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value + val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value val rootBackground by LocalRootBackgroundColor.current - return hazeEffect(hazeState, style) { + return hazeEffect(state, style) { fallbackTint = HazeTint(rootBackground) if (isGlobalBlurEnabled) { configure() @@ -78,5 +78,5 @@ fun Modifier.hazeForegroundEffectTangem( * Applies a haze source to the [Modifier] using the current global haze state. */ @Composable -fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) = - this.hazeSource(LocalHazeState.current, zIndex, key) \ No newline at end of file +fun Modifier.hazeSourceTangem(state: HazeState = LocalHazeState.current, zIndex: Float = 0f, key: Any? = null) = + this.hazeSource(state, zIndex, key) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt index 39cdb7d9ea..1f87b768f3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -1,7 +1,6 @@ package com.tangem.core.ui.components.pager import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -22,132 +21,147 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import dev.chrisbanes.haze.HazeStyle import kotlin.math.abs -import kotlin.math.min import kotlin.math.roundToInt -private const val ANIMATION_DURATION = 300 -private const val MAX_VISIBLE_DOTS = 5 +internal const val ANIMATION_DURATION = 300 +internal const val MAX_VISIBLE_DOTS = 5 +internal val SPACING = 4.dp +internal val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 private const val MIN_DISTANCE_FOR_HINT_DOT = 2 - -private val SPACING = 4.dp private val BACKGROUND_SIZE = DpSize(92.dp, 32.dp) - private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) -private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) { - val totalPages = pagerState.pageCount - val currentIndex = pagerState.currentPage + if (LocalRedesignEnabled.current) { + PagerIndicatorV2(pagerState, modifier) + } else { + PagerIndicatorV1(pagerState, modifier) + } +} +@Composable +private fun PagerIndicatorV1(pagerState: PagerState, modifier: Modifier = Modifier) { + val colors = PagerIndicatorColors( + active = TangemTheme.colors.control.key, + inactive = TangemTheme.colors.text.tertiary, + overlay = TangemTheme.colors.overlay.secondary, + ) + PagerIndicatorContent( + pagerState = pagerState, + colors = colors, + modifier = modifier, + ) +} + +@Composable +private fun PagerIndicatorV2(pagerState: PagerState, modifier: Modifier = Modifier) { + val colors = PagerIndicatorColors( + active = TangemTheme.colors2.graphic.neutral.primary, + inactive = TangemTheme.colors2.graphic.neutral.tertiary, + overlay = TangemTheme.colors2.tabs.backgroundSecondary.copy(alpha = .1f), + ) + PagerIndicatorContent( + pagerState = pagerState, + colors = colors, + modifier = modifier, + boxModifier = Modifier.hazeEffectTangem(style = HazeStyle(blurRadius = 22.dp, tint = null)), + ) +} + +@Composable +private fun rememberPagerIndicatorAnimationState(pagerState: PagerState): PagerIndicatorAnimationState { + val density = LocalDensity.current + return remember(pagerState.pageCount, density) { + PagerIndicatorAnimationState(pagerState.pageCount, pagerState.currentPage, density) + } +} + +@Suppress("LongParameterList") +private fun calculateDotAlpha( + isSliding: Boolean, + slideDirection: Int, + index: Int, + displayLower: Int, + displayUpper: Int, + fadeProgress: Float, +): Float { + return when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress + slideDirection < 0 && index == displayLower -> fadeProgress + else -> 1f + } +} + +@Composable +private fun PagerIndicatorContent( + pagerState: PagerState, + colors: PagerIndicatorColors, + modifier: Modifier = Modifier, + boxModifier: Modifier = Modifier, +) { + val totalPages = pagerState.pageCount if (totalPages == 0) return - val indicatorColor = TangemTheme.colors.control.key - val overlayColor = TangemTheme.colors.overlay.secondary - val inactiveIndicatorColor = TangemTheme.colors.text.tertiary - - val density = LocalDensity.current - - val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) - - var displayLower by remember { mutableIntStateOf(targetLower) } - var displayUpper by remember { mutableIntStateOf(targetUpper) } - var prevTargetLower by remember { mutableIntStateOf(targetLower) } - - val slideOffset = remember { Animatable(0f) } - var isSliding by remember { mutableStateOf(false) } - var slideDirection by remember { mutableIntStateOf(0) } - val fadeProgress = remember { Animatable(0f) } - var fadeJob by remember { mutableStateOf(null) } + val animState = rememberPagerIndicatorAnimationState(pagerState) + val (targetLower, targetUpper) = getWindowBounds(pagerState.pageCount, pagerState.currentPage) LaunchedEffect(targetLower) { - if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { - fadeJob?.cancel() - slideOffset.stop() - fadeProgress.stop() - - val dir = if (targetLower > prevTargetLower) 1 else -1 - val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } - val halfEdge = edgeDotSize / 2 - - isSliding = true - slideDirection = dir - fadeProgress.snapTo(0f) - - if (dir > 0) { - displayLower = prevTargetLower - displayUpper = targetUpper - slideOffset.snapTo(halfEdge) - } else { - displayLower = targetLower - displayUpper = prevTargetLower + MAX_VISIBLE_DOTS - slideOffset.snapTo(-halfEdge) - } - - prevTargetLower = targetLower - - fadeJob = launch { - fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) - } - slideOffset.animateTo( - if (dir > 0) -halfEdge else halfEdge, - tween(ANIMATION_DURATION), - ) - - displayLower = targetLower - displayUpper = targetUpper - slideOffset.snapTo(0f) - isSliding = false - slideDirection = 0 - } + animState.onBoundsChange(this, targetLower, targetUpper) } - val visibleIndices = (displayLower until displayUpper).toList() + + val visibleIndices = (animState.displayLower until animState.displayUpper).toList() Box( modifier = modifier .width(BACKGROUND_SIZE.width) .height(BACKGROUND_SIZE.height) .background( - color = overlayColor, + color = colors.overlay, shape = CircleShape, ) - .clip(CircleShape), + .clip(CircleShape) + .then(boxModifier), contentAlignment = Alignment.Center, ) { Row( modifier = Modifier.offset { - IntOffset(slideOffset.value.roundToInt(), 0) + IntOffset(animState.slideOffset.value.roundToInt(), 0) }, horizontalArrangement = Arrangement.spacedBy(SPACING), verticalAlignment = Alignment.CenterVertically, ) { visibleIndices.forEach { index -> - val dotAlpha = when { - !isSliding -> 1f - slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value - slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value - slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value - slideDirection < 0 && index == displayLower -> fadeProgress.value - else -> 1f - } + val dotAlpha = calculateDotAlpha( + isSliding = animState.isSliding, + slideDirection = animState.slideDirection, + index = index, + displayLower = animState.displayLower, + displayUpper = animState.displayUpper, + fadeProgress = animState.fadeProgress.value, + ) key(index) { Dot( index = index, - currentIndex = currentIndex, + currentIndex = pagerState.currentPage, totalPages = totalPages, - activeColor = indicatorColor, - inactiveColor = inactiveIndicatorColor, + activeColor = colors.active, + inactiveColor = colors.inactive, modifier = Modifier.graphicsLayer { alpha = dotAlpha }, ) } @@ -156,19 +170,6 @@ fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) { } } -private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { - if (totalPages <= MAX_VISIBLE_DOTS) { - return 0 to totalPages - } - val lowerBound = when { - currentIndex <= 1 -> 0 - currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS - else -> currentIndex - 2 - } - val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) - return lowerBound to upperBound -} - private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { if (index == currentIndex) { return CURRENT_DOT_SIZE @@ -180,6 +181,46 @@ private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { return params.calculateSize() } +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) +} + +@Immutable +private data class PagerIndicatorColors( + val active: Color, + val inactive: Color, + val overlay: Color, +) + private class DotSizeParams private constructor( val posInWindow: Int, val currentPosInWindow: Int, @@ -248,43 +289,27 @@ private class DotSizeParams private constructor( } } +@Preview(showBackground = true) @Composable -private fun Dot( - index: Int, - currentIndex: Int, - totalPages: Int, - activeColor: Color, - inactiveColor: Color, - modifier: Modifier = Modifier, -) { - val isActive = index == currentIndex - val size = getDotSize(index, currentIndex, totalPages) - - val animSpec = tween(ANIMATION_DURATION) - val colorSpec = tween(ANIMATION_DURATION) - - val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") - val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") - val animatedColor by animateColorAsState( - targetValue = if (isActive) activeColor else inactiveColor, - animationSpec = colorSpec, - label = "c$index", - ) - - val shape = RoundedCornerShape(animatedHeight / 2) - - Box( - modifier = modifier - .width(animatedWidth) - .height(animatedHeight) - .background(animatedColor, shape), - ) +private fun PagerIndicatorPreviewV1() { + TangemThemePreview { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4).forEach { page -> + PagerIndicator(rememberPagerState(page) { 5 }) + } + } + } } @Preview(showBackground = true) @Composable -private fun PagerIndicatorPreview() { - TangemThemePreview { +private fun PagerIndicatorPreviewV2() { + TangemThemePreviewRedesign { Column( Modifier .background(TangemTheme.colors.background.primary) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt new file mode 100644 index 0000000000..1968908ace --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt @@ -0,0 +1,95 @@ +package com.tangem.core.ui.components.pager + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.runtime.* +import androidx.compose.ui.unit.Density +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.min + +@Stable +internal class PagerIndicatorAnimationState( + private val totalPages: Int, + initialCurrentPage: Int, + private val density: Density, +) { + var displayLower by mutableIntStateOf(0) + private set + var displayUpper by mutableIntStateOf(0) + private set + + val slideOffset = Animatable(0f) + var isSliding by mutableStateOf(false) + private set + var slideDirection by mutableIntStateOf(0) + private set + val fadeProgress = Animatable(0f) + private var fadeJob by mutableStateOf(null) + + private var prevTargetLower by mutableIntStateOf(0) + + init { + val (lower, upper) = getWindowBounds(totalPages, initialCurrentPage) + displayLower = lower + displayUpper = upper + prevTargetLower = lower + } + + suspend fun onBoundsChange(scope: CoroutineScope, targetLower: Int, targetUpper: Int) { + if (targetLower == prevTargetLower || totalPages <= MAX_VISIBLE_DOTS) { + return + } + + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) + } + + prevTargetLower = targetLower + + fadeJob = scope.launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } +} + +internal fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages + } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 + } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt index 2c05dbfe7d..1d0dbc825f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt @@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle { override fun createShader(size: Size): Shader { val center = Offset(size.width / 2f, size.height / 2f) val diagonal = sqrt(size.width * size.width + size.height * size.height) - val direction = Offset(x = 1f, y = 0.5f) - val halfDist = diagonal / 2f - val baseStart = center - direction * halfDist - val baseEnd = center + direction * halfDist - val shift = direction * offset * diagonal + // Subtle diagonal angle, similar to iOS shimmer + val direction = Offset(x = 1f, y = 0.3f) + // Half-width of the blob (80% of diagonal total — wide, soft sweep) + val bandHalf = diagonal * 0.40f + + // Sweep the highlight center from left-of-element to right-of-element. + // offset 0..1 maps to a full pass including off-screen padding on both sides. + val shift = direction * ((offset - 0.5f) * diagonal * 1.5f) + val highlightCenter = center + shift + + // Full color text with a wide, gradual low-alpha dip sweeping left → right return LinearGradientShader( - colors = listOf(textColor.copy(alpha = 0.2f), textColor), - from = baseStart + shift, - to = baseEnd + shift, - colorStops = listOf(0.0f, 0.15f), - tileMode = TileMode.Mirror, + colors = listOf( + textColor, + textColor.copy(alpha = 0.75f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.3f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.75f), + textColor, + ), + from = highlightCenter - direction * bandHalf, + to = highlightCenter + direction * bandHalf, + colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f), + tileMode = TileMode.Clamp, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 7797159dc1..c09e64697b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** Tokens list item state */ @Immutable @@ -45,15 +46,34 @@ sealed interface TokensListItemUM { val tokenItemUM: TokenItemState, val isExpanded: Boolean, val isCollapsable: Boolean, - val tokens: ImmutableList, + val content: PortfolioItemContentUM, ) : TokensListItemUM { override val id: String = tokenItemUM.id + + val tokens: ImmutableList + get() = when (content) { + is PortfolioItemContentUM.Tokens -> content.tokens + is PortfolioItemContentUM.Empty -> persistentListOf() + } } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM } +@Immutable sealed interface PortfolioTokensListItemUM { /** Unique ID */ val id: Any +} + +@Immutable +sealed interface PortfolioItemContentUM { + data class Tokens(val tokens: ImmutableList) : PortfolioItemContentUM + data class Empty(val action: Action? = null) : PortfolioItemContentUM { + + data class Action( + val text: TextReference, + val onClick: () -> Unit, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt new file mode 100644 index 0000000000..5528190513 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt @@ -0,0 +1,370 @@ +package com.tangem.core.ui.ds + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt + +private const val ANIMATION_DURATION = 300 +private const val MAX_VISIBLE_DOTS = 5 +private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 +private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 +private const val MIN_DISTANCE_FOR_HINT_DOT = 2 + +private val SPACING = 4.dp +private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) +private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) +private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) +private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) + +/** + * // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation. + * + * A pager indicator that adapts to the number of pages and the current page index. + * + * For 5 or fewer pages, it shows all dots with the current page highlighted. + * For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position. + * + * @param pagerState state of the pager to observe + * @param activeIndicatorColor color for the active page indicator + * @param inactiveIndicatorColor color for the inactive page indicators + * @param modifier modifier for styling + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun TangemPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier, + activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary, + inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary, +) { + val totalPages = pagerState.pageCount + val currentIndex = pagerState.currentPage + + if (totalPages == 0) return + + val density = LocalDensity.current + + val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) + + var displayLower by remember { mutableIntStateOf(targetLower) } + var displayUpper by remember { mutableIntStateOf(targetUpper) } + var prevTargetLower by remember { mutableIntStateOf(targetLower) } + + val slideOffset = remember { Animatable(0f) } + var isSliding by remember { mutableStateOf(false) } + var slideDirection by remember { mutableIntStateOf(0) } + val fadeProgress = remember { Animatable(0f) } + var fadeJob by remember { mutableStateOf(null) } + + LaunchedEffect(targetLower) { + if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayLower = prevTargetLower + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) + } + + prevTargetLower = targetLower + + fadeJob = launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } + } + val visibleIndices = (displayLower until displayUpper).toList() + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.offset { + IntOffset(slideOffset.value.roundToInt(), 0) + }, + horizontalArrangement = Arrangement.spacedBy(SPACING), + verticalAlignment = Alignment.CenterVertically, + ) { + visibleIndices.forEach { index -> + val dotAlpha = when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value + slideDirection < 0 && index == displayLower -> fadeProgress.value + else -> 1f + } + + key(index) { + Dot( + index = index, + currentIndex = currentIndex, + totalPages = totalPages, + activeColor = activeIndicatorColor, + inactiveColor = inactiveIndicatorColor, + modifier = Modifier.graphicsLayer { alpha = dotAlpha }, + ) + } + } + } + } +} + +private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages + } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 + } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound +} + +private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { + if (index == currentIndex) { + return CURRENT_DOT_SIZE + } + if (totalPages <= MAX_VISIBLE_DOTS) { + return NORMAL_DOT_SIZE + } + val params = DotSizeParams.create(index, currentIndex, totalPages) + return params.calculateSize() +} + +private class DotSizeParams private constructor( + val posInWindow: Int, + val currentPosInWindow: Int, + val hiddenLeft: Int, + val hiddenRight: Int, + val distanceFromCurrent: Int, +) { + private val lastPos = MAX_VISIBLE_DOTS - 1 + private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1 + + fun calculateSize(): DpSize = when { + isCentered -> getCenteredSize() + hiddenRight >= 1 -> getRightEdgeSize() + hiddenLeft >= 1 -> getLeftEdgeSize() + else -> NORMAL_DOT_SIZE + } + + private fun getCenteredSize(): DpSize = when (posInWindow) { + 0, lastPos -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + + private fun getRightEdgeSize(): DpSize { + val isLastPos = posInWindow == lastPos + val isSecondToLast = posInWindow == lastPos - 1 + val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isLastPos && isModerateDistance -> HINT_DOT_SIZE + isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + private fun getLeftEdgeSize(): DpSize { + val isFirstPos = posInWindow == 0 + val isSecondPos = posInWindow == 1 + val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isFirstPos && isModerateDistance -> HINT_DOT_SIZE + isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + companion object { + fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams { + val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex) + val posInWindow = index - windowStart + val currentPosInWindow = currentIndex - windowStart + return DotSizeParams( + posInWindow = posInWindow, + currentPosInWindow = currentPosInWindow, + hiddenLeft = windowStart, + hiddenRight = totalPages - windowEnd, + distanceFromCurrent = abs(posInWindow - currentPosInWindow), + ) + } + } +} + +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 5 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator6ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 6 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator7ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 7 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator10ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 10 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorSmallCountsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + TangemPagerIndicator(rememberPagerState(0) { 1 }) + TangemPagerIndicator(rememberPagerState(1) { 2 }) + TangemPagerIndicator(rememberPagerState(1) { 3 }) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index 7f9f95b4d3..b8939058d7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -1,13 +1,11 @@ package com.tangem.core.ui.ds.badge import android.content.res.Configuration -import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -15,18 +13,17 @@ 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.graphics.Shape -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.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.badge.TangemBadgeSize.* -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -43,7 +40,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { TangemBadge( text = badgeUM.text, - iconRes = badgeUM.iconRes, + tangemIconUM = badgeUM.tangemIconUM, size = badgeUM.size, shape = badgeUM.shape, color = badgeUM.color, @@ -60,7 +57,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { * * @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 tangemIconUM Model of representation 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. @@ -72,14 +69,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { */ @Composable fun TangemBadge( - text: TextReference, modifier: Modifier = Modifier, - @DrawableRes iconRes: Int? = null, + text: TextReference? = null, + tangemIconUM: TangemIconUM? = null, size: TangemBadgeSize = X9, shape: TangemBadgeShape = TangemBadgeShape.Default, color: TangemBadgeColor = TangemBadgeColor.Gray, type: TangemBadgeType = TangemBadgeType.Solid, - iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None, onClick: (() -> Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) @@ -93,37 +90,84 @@ fun TangemBadge( .padding(size.toPaddingDp(position = iconPosition)) .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start, - modifier = Modifier.size(size = size.toContentSize()), - label = "Start Icon Visibility", - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - Icon( - painter = painterResource(id = wrappedIconRes), - contentDescription = null, - tint = iconColor, - ) - } - Text( - text = text.resolveReference(), - style = size.toTextStyle(), - maxLines = 1, - color = getTextColor(type = type, color = color), + StartIcon( + tangemIconUM = tangemIconUM, + iconPosition = iconPosition, + size = size, + iconColor = iconColor, ) - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End, - modifier = Modifier.size(size = size.toContentSize()), - label = "End Icon Visibility", + visible = text != null, + label = "Text Visibility", ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - Icon( - painter = painterResource(id = wrappedIconRes), - contentDescription = null, - tint = iconColor, + val wrappedText = remember(this) { requireNotNull(text) } + Text( + text = wrappedText.resolveReference(), + style = size.toTextStyle(), + maxLines = 1, + color = getTextColor(type = type, color = color), ) } + EndIcon( + tangemIconUM = tangemIconUM, + iconPosition = iconPosition, + size = size, + iconColor = iconColor, + ) + } +} + +@Composable +private fun StartIcon( + iconPosition: TangemBadgeIconPosition, + size: TangemBadgeSize, + iconColor: Color, + tangemIconUM: TangemIconUM? = null, +) { + AnimatedVisibility( + visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + label = "Start Icon Visibility", + ) { + val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) } + TangemIcon( + modifier = Modifier.fillMaxSize(), + tangemIconUM = when (wrappedIconRes) { + is TangemIconUM.Currency, + is TangemIconUM.Ident, + is TangemIconUM.Image, + is TangemIconUM.Url, + -> wrappedIconRes + is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + }, + ) + } +} + +@Composable +private fun EndIcon( + iconPosition: TangemBadgeIconPosition, + size: TangemBadgeSize, + iconColor: Color, + tangemIconUM: TangemIconUM? = null, +) { + AnimatedVisibility( + visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + label = "End Icon Visibility", + ) { + val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) } + TangemIcon( + modifier = Modifier.fillMaxSize(), + tangemIconUM = when (wrappedIconRes) { + is TangemIconUM.Currency, + is TangemIconUM.Ident, + is TangemIconUM.Image, + is TangemIconUM.Url, + -> wrappedIconRes + is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + }, + ) } } @@ -178,14 +222,17 @@ enum class TangemBadgeSize { X4 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp) } X6 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp) } X9 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp) } } @@ -222,6 +269,7 @@ enum class TangemBadgeSize { enum class TangemBadgeIconPosition { Start, End, + None, } /** @@ -240,6 +288,7 @@ enum class TangemBadgeColor { Blue, Red, Gray, + Green, } @ReadOnlyComposable @@ -258,6 +307,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.iconRed TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.iconGreen + TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant + } } @ReadOnlyComposable @@ -276,8 +331,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.textRed TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.textGreen + TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant + } } +@Suppress("CyclomaticComplexMethod") @ReadOnlyComposable @Composable private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) { @@ -286,6 +348,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen }, ) TangemBadgeType.Tinted -> background( @@ -293,6 +356,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen }, ) TangemBadgeType.Outline -> { @@ -301,6 +365,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen }, shape = shape, width = 1.dp, @@ -320,16 +385,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl .background(TangemTheme.colors2.surface.level1) .padding(8.dp), ) { - repeat(2) { yIndex -> + repeat(3) { yIndex -> Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { repeat(TangemBadgeType.entries.size) { index -> TangemBadge( - text = stringReference("Title"), - iconRes = R.drawable.ic_information_24, + text = stringReference("Title").takeIf { yIndex < 2 }, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24), type = TangemBadgeType.entries[index], color = params, shape = TangemBadgeShape.entries[yIndex % 2], - iconPosition = TangemBadgeIconPosition.entries[yIndex % 2], + iconPosition = TangemBadgeIconPosition.entries[yIndex], ) } } @@ -344,6 +409,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider TangemTheme.colors2.text.status.disabled @@ -70,7 +73,8 @@ fun GhostTangemButton( } TangemButtonInternal( onClick = onClick, - modifier = modifier, + modifier = modifier + .clip(shape = shape.toShape(size)), text = text, contentColor = contentColor, enabled = enabled, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index 4d6f5dc9d9..b9426982dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -7,10 +7,10 @@ import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,72 +60,91 @@ internal fun TangemButtonInternal( size: TangemButtonSize = TangemButtonSize.X15, state: TangemButtonState = TangemButtonState.Default, ) { - Row( - modifier = modifier - .testTag(BaseButtonTestTags.BUTTON) - .height(size.toHeightDp()) - .conditionalCompose(text == null) { - width(size.toHeightDp()) + ProvideButtonRippleConfiguration { + Row( + modifier = modifier + .testTag(BaseButtonTestTags.BUTTON) + .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button) + .height(size.toHeightDp()) + .conditionalCompose(text == null) { + width(size.toHeightDp()) + } + .conditionalCompose(text != null) { + padding(horizontal = size.toPaddingDp()) + } + .animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility( + visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start, + modifier = Modifier.size(size = size.toContentSize()), + ) { + val wrappedIconRes = remember(this, iconRes) { requireNotNull(iconRes) } + TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) } - .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button) - .conditionalCompose(text != null) { - padding(horizontal = size.toPaddingDp()) + + AnimatedVisibility(text != null && state != TangemButtonState.Loading) { + val wrappedText = remember(this) { requireNotNull(text) } + val textStyle = size.toTextStyle() + Text( + text = wrappedText.resolveReference(), + style = textStyle, + color = contentColor, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = textStyle.fontSize, + ), + modifier = Modifier.testTag(BaseButtonTestTags.TEXT), + ) } - .animateContentSize(), - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, + + AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) { + val wrappedText = remember(this) { requireNotNull(descriptionText) } + val textStyle = TangemTheme.typography2.captionSemibold12 + Text( + text = wrappedText.resolveReference(), + style = textStyle, + color = TangemTheme.colors2.text.status.disabled, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = textStyle.fontSize, + ), + modifier = Modifier.testTag(BaseButtonTestTags.TEXT), + ) + } + + AnimatedVisibility( + visible = iconRes != null && iconPosition == TangemButtonIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + ) { + val wrappedIconRes = remember(this) { requireNotNull(iconRes) } + TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) + } + } + } +} + +@Composable +private inline fun ProvideButtonRippleConfiguration(crossinline content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalRippleConfiguration provides RippleConfiguration( + color = TangemTheme.colors2.overlay.overlaySecondary, + RippleAlpha( + pressedAlpha = 0.4f, + focusedAlpha = 0.4f, + draggedAlpha = 0.4f, + hoveredAlpha = 0.4f, + ), + ), ) { - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start, - modifier = Modifier.size(size = size.toContentSize()), - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) - } - - AnimatedVisibility(text != null && state != TangemButtonState.Loading) { - val wrappedText = remember(this) { requireNotNull(text) } - val textStyle = size.toTextStyle() - Text( - text = wrappedText.resolveReference(), - style = textStyle, - color = contentColor, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - autoSize = TextAutoSize.StepBased( - minFontSize = 12.sp, - maxFontSize = textStyle.fontSize, - ), - modifier = Modifier.testTag(BaseButtonTestTags.TEXT), - ) - } - - AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) { - val wrappedText = remember(this) { requireNotNull(descriptionText) } - val textStyle = TangemTheme.typography2.captionSemibold12 - Text( - text = wrappedText.resolveReference(), - style = textStyle, - color = TangemTheme.colors2.text.status.disabled, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - autoSize = TextAutoSize.StepBased( - minFontSize = 12.sp, - maxFontSize = textStyle.fontSize, - ), - modifier = Modifier.testTag(BaseButtonTestTags.TEXT), - ) - } - - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemButtonIconPosition.End, - modifier = Modifier.size(size = size.toContentSize()), - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) - } + content() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt index 0b0e426629..a3bb16c4da 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.ds.button import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.TextReference /** @@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference * [REDACTED_AUTHOR] */ +@Stable data class TangemButtonUM( val text: TextReference? = null, val descriptionText: TextReference? = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt index 9abdda6653..f1bf0d3fdb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt @@ -48,9 +48,9 @@ fun TangemCheckbox( isEnabled: Boolean = true, ) { val shape = if (isRounded) { - RoundedCornerShape(TangemTheme.dimens2.x1) - } else { CircleShape + } else { + RoundedCornerShape(TangemTheme.dimens2.x1) } Box( modifier = modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt new file mode 100644 index 0000000000..69a475c2a8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt @@ -0,0 +1,288 @@ +package com.tangem.core.ui.ds.contextmenu + +import android.content.res.Configuration +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +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.PopUpMenuTestTags +import dev.chrisbanes.haze.rememberHazeState +import kotlin.math.max +import kotlin.math.min + +/** + * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. + */ +@Composable +fun TangemContextMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset.Zero, + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit, +) { + val expandedStates = remember { MutableTransitionState(false) } + expandedStates.targetState = expanded + + if (expandedStates.currentState || expandedStates.targetState) { + val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } + val density = LocalDensity.current + val popupPositionProvider = DropdownMenuPositionProvider( + offset, + density, + ) { parentBounds, menuBounds -> + transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds) + } + + Popup( + onDismissRequest = onDismissRequest, + popupPositionProvider = popupPositionProvider, + properties = properties, + ) { + DropdownMenuContent( + expandedStates = expandedStates, + transformOriginState = transformOriginState, + modifier = modifier, + content = content, + ) + } + } +} + +private const val IN_TRANSITION_DURATION = 120 +private const val OUT_TRANSITION_DURATION = 75 + +@Suppress("ReusedModifierInstance", "MagicNumber") +@Composable +private fun DropdownMenuContent( + expandedStates: MutableTransitionState, + transformOriginState: MutableState, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + // Menu open/close animation. + val transition = rememberTransition(expandedStates, "DropDownMenu") + + val scale by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween( + durationMillis = IN_TRANSITION_DURATION, + easing = LinearOutSlowInEasing, + ) + } else { + // Expanded to dismissed. + tween( + durationMillis = 1, + delayMillis = OUT_TRANSITION_DURATION - 1, + ) + } + }, + label = "", + ) { isExpanded -> + if (isExpanded) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0.8f + } + } + + val alpha by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween(durationMillis = 30) + } else { + // Expanded to dismissed. + tween(durationMillis = OUT_TRANSITION_DURATION) + } + }, + label = "", + ) { isExpanded -> + if (isExpanded) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0f + } + } + Card( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens2.x5)) + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + transformOrigin = transformOriginState.value + }, + elevation = CardDefaults.cardElevation(), + ) { + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .verticalScroll(rememberScrollState()) + .clip(RoundedCornerShape(TangemTheme.dimens2.x5)) + .background(TangemTheme.colors2.contextMenu.background) + .testTag(PopUpMenuTestTags.CONTAINER), + content = content, + ) + } +} + +private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { + val pivotX = when { + menuBounds.left >= parentBounds.right -> 0f + menuBounds.right <= parentBounds.left -> 1f + menuBounds.width == 0 -> 0f + else -> { + val intersectionCenter = + (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 + (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width + } + } + val pivotY = when { + menuBounds.top >= parentBounds.bottom -> 0f + menuBounds.bottom <= parentBounds.top -> 1f + menuBounds.height == 0 -> 0f + else -> { + val intersectionCenter = + (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 + (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height + } + } + return TransformOrigin(pivotX, pivotY) +} + +private val MenuVerticalMargin = 48.dp + +@Immutable +internal data class DropdownMenuPositionProvider( + val contentOffset: DpOffset, + val density: Density, + val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + // The min margin above and below the menu, relative to the screen. + val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() } + // The content offset specified using the dropdown offset parameter. + val contentOffsetX = with(density) { contentOffset.x.roundToPx() } + val contentOffsetY = with(density) { contentOffset.y.roundToPx() } + + // Compute horizontal position. + val toRight = anchorBounds.left + contentOffsetX + val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width + val toDisplayRight = windowSize.width - popupContentSize.width + val toDisplayLeft = 0 + val x = if (layoutDirection == LayoutDirection.Ltr) { + sequenceOf( + toRight, + toLeft, + // If the anchor gets outside of the window on the left, we want to position + // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight. + if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft, + ) + } else { + sequenceOf( + toLeft, + toRight, + // If the anchor gets outside of the window on the right, we want to position + // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft. + if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight, + ) + }.firstOrNull { + it >= 0 && it + popupContentSize.width <= windowSize.width + } ?: toLeft + + // Compute vertical position. + val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) + val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height + val toCenter = anchorBounds.top - popupContentSize.height / 2 + val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin + val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { element -> + element >= verticalMargin && + element + popupContentSize.height <= windowSize.height - verticalMargin + } ?: toTop + + onPositionCalculated( + anchorBounds, + IntRect( + left = x, + top = y, + right = x + popupContentSize.width, + bottom = y + popupContentSize.height, + ), + ) + return IntOffset(x, y) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemContextMenu_Preview() { + TangemThemePreviewRedesign { + val hazeState = rememberHazeState() + Column( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1) + .hazeSourceTangem(state = hazeState, zIndex = -1f), + ) { + TangemContextMenu( + expanded = true, + onDismissRequest = { }, + modifier = Modifier.hazeEffectTangem(state = hazeState), + ) { + TangemContextMenuCheckboxItem( + title = stringReference("Sort by balance"), + isChecked = true, + onClick = {}, + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors2.border.neutral.quaternary, + ) + TangemContextMenuCheckboxItem( + title = stringReference("Group tokens"), + isChecked = false, + onClick = {}, + ) + } + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt new file mode 100644 index 0000000000..8cb5703384 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt @@ -0,0 +1,43 @@ +package com.tangem.core.ui.ds.contextmenu + +import androidx.compose.foundation.layout.* +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.ds.checkbox.TangemCheckbox +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Item with checkbox for [TangemContextMenu]. + */ +@Composable +fun TangemContextMenuCheckboxItem(title: TextReference, isChecked: Boolean, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .width(238.dp) + .clickableSingle(onClick = onClick) + .padding( + vertical = TangemTheme.dimens2.x5, + horizontal = TangemTheme.dimens2.x4, + ), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography2.headingSemibold17, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + TangemCheckbox( + modifier = Modifier, + isRounded = true, + isChecked = isChecked, + onCheckedChange = { onClick() }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt new file mode 100644 index 0000000000..6d6d6f046d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt @@ -0,0 +1,202 @@ +package com.tangem.core.ui.ds.image + +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.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +/** + * Composable function for displaying a wallet icon based on the provided [DeviceIconUM] state. + * + * The icon can represent different types of devices, such as cards, rings, stubs, or mobile wallet, + * with customizable colors and styles. + * + * @param state The state of the device icon, which determines its appearance. + * @param modifier Optional [Modifier] for styling the composable. + */ +@Composable +fun TangemDeviceIcon(state: DeviceIconUM, modifier: Modifier = Modifier) { + when (state) { + is DeviceIconUM.Card -> DeviceIcon( + modifier = modifier, + isRing = false, + mainColor = state.mainColor, + secondColor = state.secondColor, + thirdColor = state.thirdColor, + tColor = null, + ) + is DeviceIconUM.Ring -> DeviceIcon( + modifier = modifier, + isRing = true, + mainColor = state.mainColor, + secondColor = state.cardColor, + thirdColor = state.secondCardColor, + tColor = null, + ) + is DeviceIconUM.Stub -> DeviceIcon( + modifier = modifier, + isRing = false, + mainColor = Color.Unspecified, + secondColor = Color.Unspecified.takeIf { state.cardsCount > 1 }, + thirdColor = Color.Unspecified.takeIf { state.cardsCount > 2 }, + tColor = TangemTheme.colors2.graphic.neutral.secondary, + ) + DeviceIconUM.Mobile -> Icon( + modifier = modifier, + imageVector = ImageVector.vectorResource(R.drawable.ic_shield_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.status.attention, + ) + } +} + +@Composable +private fun DeviceIcon( + isRing: Boolean, + mainColor: Color, + secondColor: Color?, + thirdColor: Color?, + tColor: Color?, + modifier: Modifier = Modifier, +) { + val main = mainColor.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val second = secondColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val third = thirdColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val borderColor = TangemTheme.colors2.border.walletIcon + + val imageVector = remember(isRing, main, second, third, borderColor, tColor) { + when { + isRing && second != null && third != null -> WalletIconVectorBuilders.buildRingWithCard2( + mainColor = main, + cardColor = second, + secondCardColor = third, + borderColor = borderColor, + ) + !isRing && second != null && third != null -> WalletIconVectorBuilders.buildCard3( + mainColor = main, + secondColor = second, + thirdColor = third, + tColor = tColor, + borderColor = borderColor, + ) + isRing && second != null -> WalletIconVectorBuilders.buildRingWithCard( + mainColor = main, + cardColor = second, + borderColor = borderColor, + ) + !isRing && second != null -> WalletIconVectorBuilders.buildCard2( + mainColor = main, + secondColor = second, + tColor = tColor, + borderColor = borderColor, + ) + isRing -> WalletIconVectorBuilders.buildRing( + mainColor = main, + borderColor = borderColor, + ) + else -> WalletIconVectorBuilders.buildCard( + mainColor = main, + borderColor = borderColor, + tColor = tColor, + ) + } + } + + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = modifier, + tint = Color.Unspecified, + ) +} + +// region Preview + +private val previewCardBlue + get() = Color(0xFF1C5FBF) +private val previewCardGold + get() = Color(0xFFD4A017) +private val previewCardPurple + get() = Color(0xFF7B2FBE) +private val previewRingGreen + get() = Color(0xFF2ECC71) + +private val previewStates: List> + get() = listOf( + "Card 1" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = null, + ), + "Card 2" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = previewCardGold, + ), + "Card 3" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = previewCardGold, + thirdColor = previewCardPurple, + ), + "Ring" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + ), + "Ring + Card" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + cardColor = previewCardBlue, + ), + "Ring + 2 Cards" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + cardColor = previewCardBlue, + secondCardColor = previewCardGold, + ), + "Stub 1" to DeviceIconUM.Stub(cardsCount = 1), + "Stub 2" to DeviceIconUM.Stub(cardsCount = 2), + "Stub 3" to DeviceIconUM.Stub(cardsCount = 3), + "Mobile" to DeviceIconUM.Mobile, + ) + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemDeviceIcon_Preview() { + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + previewStates.forEach { (label, state) -> + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TangemDeviceIcon( + modifier = Modifier.size(40.dp), + state = state, + ) + Text( + text = label, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt new file mode 100644 index 0000000000..0007a0f1a8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt @@ -0,0 +1,24 @@ +package com.tangem.core.ui.ds.image + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +sealed interface DeviceIconUM { + + data class Card( + val mainColor: Color, + val secondColor: Color?, + val thirdColor: Color? = null, + ) : DeviceIconUM + + data class Ring( + val mainColor: Color = Color.Unspecified, + val cardColor: Color? = null, + val secondCardColor: Color? = null, + ) : DeviceIconUM + + data class Stub(val cardsCount: Int) : DeviceIconUM + + data object Mobile : DeviceIconUM +} \ 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 index ff7be95be2..2c6ecb3b07 100644 --- 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 @@ -2,12 +2,19 @@ package com.tangem.core.ui.ds.image import androidx.annotation.DrawableRes import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape 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.platform.LocalContext import androidx.compose.ui.res.vectorResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer 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 @@ -40,6 +47,11 @@ sealed interface TangemIconUM { data class Ident( val text: String, ) : TangemIconUM + + /** Image represented from network by url */ + data class Url( + val url: String, + ) : TangemIconUM } /** @@ -72,5 +84,24 @@ fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) { address = tangemIconUM.text, modifier = modifier, ) + is TangemIconUM.Url -> SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(tangemIconUM.url) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors2.surface.level3, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt new file mode 100644 index 0000000000..993fdf132f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt @@ -0,0 +1,1131 @@ +@file:Suppress("MagicNumber", "LargeClass", "LongMethod", "NamedArguments") +package com.tangem.core.ui.ds.image + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +/** + * Builders for creating [ImageVector] instances for wallet icons. + * These builders are used to generate the vector graphics for the wallet icons + * + * Generated based on SVG paths. + */ +internal object WalletIconVectorBuilders { + + fun buildRing(mainColor: Color, borderColor: Color): ImageVector = ImageVector.Builder( + name = "Ring", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(10.333f, 3f) + curveTo(7.94f, 3f, 6f, 7.029f, 6f, 12f) + curveTo(6f, 16.971f, 7.94f, 21f, 10.333f, 21f) + horizontalLineTo(14.667f) + curveTo(17.06f, 21f, 19f, 16.971f, 19f, 12f) + curveTo(19f, 7.029f, 17.06f, 3f, 14.667f, 3f) + horizontalLineTo(10.333f) + close() + moveTo(10.574f, 3.9f) + curveTo(16.403f, 3.9f, 16.498f, 20.1f, 10.574f, 20.1f) + curveTo(10.541f, 20.1f, 10.539f, 20.052f, 10.571f, 20.045f) + curveTo(11.037f, 19.937f, 11.478f, 19.675f, 11.883f, 19.285f) + curveTo(11.961f, 19.21f, 11.96f, 19.087f, 11.889f, 19.005f) + curveTo(10.671f, 17.602f, 9.852f, 14.99f, 9.852f, 12f) + curveTo(9.852f, 9.009f, 10.671f, 6.397f, 11.89f, 4.994f) + curveTo(11.961f, 4.913f, 11.961f, 4.789f, 11.883f, 4.714f) + curveTo(11.479f, 4.324f, 11.037f, 4.062f, 10.571f, 3.955f) + curveTo(10.539f, 3.948f, 10.541f, 3.9f, 10.574f, 3.9f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(14.667f, 3.5f) + horizontalLineTo(11.398f) + curveTo(12.705f, 3.818f, 13.671f, 4.88f, 14.316f, 6.213f) + curveTo(15.094f, 7.818f, 15.475f, 9.923f, 15.481f, 11.998f) + curveTo(15.488f, 14.073f, 15.118f, 16.179f, 14.343f, 17.786f) + curveTo(13.698f, 19.122f, 12.728f, 20.183f, 11.407f, 20.5f) + horizontalLineTo(14.667f) + curveTo(15.562f, 20.5f, 16.518f, 19.73f, 17.28f, 18.147f) + curveTo(18.025f, 16.6f, 18.5f, 14.427f, 18.5f, 12f) + curveTo(18.5f, 9.573f, 18.025f, 7.4f, 17.28f, 5.853f) + curveTo(16.518f, 4.27f, 15.562f, 3.5f, 14.667f, 3.5f) + close() + moveTo(10.264f, 3.503f) + curveTo(9.389f, 3.542f, 8.462f, 4.311f, 7.72f, 5.853f) + curveTo(6.975f, 7.4f, 6.5f, 9.573f, 6.5f, 12f) + curveTo(6.5f, 14.427f, 6.975f, 16.6f, 7.72f, 18.147f) + curveTo(8.462f, 19.688f, 9.388f, 20.457f, 10.263f, 20.496f) + curveTo(10.238f, 20.478f, 10.212f, 20.458f, 10.19f, 20.435f) + curveTo(10.094f, 20.332f, 10.054f, 20.208f, 10.049f, 20.101f) + curveTo(10.038f, 19.888f, 10.168f, 19.625f, 10.459f, 19.558f) + curveTo(10.748f, 19.491f, 11.04f, 19.341f, 11.329f, 19.106f) + curveTo(10.111f, 17.543f, 9.352f, 14.913f, 9.352f, 12f) + curveTo(9.352f, 9.086f, 10.111f, 6.455f, 11.33f, 4.892f) + curveTo(11.041f, 4.657f, 10.748f, 4.509f, 10.459f, 4.442f) + curveTo(10.171f, 4.376f, 10.038f, 4.115f, 10.049f, 3.898f) + curveTo(10.055f, 3.79f, 10.096f, 3.666f, 10.192f, 3.563f) + curveTo(10.214f, 3.54f, 10.239f, 3.521f, 10.264f, 3.503f) + close() + } + }.build() + + fun buildRingWithCard(mainColor: Color, cardColor: Color, borderColor: Color): ImageVector = ImageVector.Builder( + name = "RingWithCard", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(4.667f, 10f) + curveTo(3.194f, 10f, 2f, 12.462f, 2f, 15.5f) + curveTo(2f, 18.538f, 3.194f, 21f, 4.667f, 21f) + horizontalLineTo(7.333f) + curveTo(8.806f, 21f, 10f, 18.538f, 10f, 15.5f) + curveTo(10f, 12.462f, 8.806f, 10f, 7.333f, 10f) + horizontalLineTo(4.667f) + close() + moveTo(4.815f, 10.55f) + curveTo(8.402f, 10.55f, 8.46f, 20.45f, 4.815f, 20.45f) + curveTo(4.795f, 20.45f, 4.793f, 20.421f, 4.813f, 20.416f) + curveTo(5.099f, 20.351f, 5.371f, 20.19f, 5.62f, 19.952f) + curveTo(5.668f, 19.906f, 5.668f, 19.83f, 5.624f, 19.781f) + curveTo(4.874f, 18.923f, 4.37f, 17.327f, 4.37f, 15.5f) + curveTo(4.37f, 13.672f, 4.875f, 12.076f, 5.624f, 11.219f) + curveTo(5.668f, 11.169f, 5.668f, 11.093f, 5.62f, 11.047f) + curveTo(5.371f, 10.809f, 5.1f, 10.649f, 4.813f, 10.584f) + curveTo(4.793f, 10.579f, 4.795f, 10.55f, 4.815f, 10.55f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(7.333f, 10.5f) + horizontalLineTo(6.192f) + curveTo(6.657f, 10.835f, 7.021f, 11.328f, 7.29f, 11.878f) + curveTo(7.785f, 12.893f, 8.023f, 14.21f, 8.027f, 15.498f) + curveTo(8.031f, 16.786f, 7.801f, 18.106f, 7.307f, 19.122f) + curveTo(7.038f, 19.674f, 6.674f, 20.166f, 6.207f, 20.5f) + horizontalLineTo(7.333f) + curveTo(7.77f, 20.5f, 8.31f, 20.119f, 8.77f, 19.171f) + curveTo(9.212f, 18.257f, 9.5f, 16.96f, 9.5f, 15.5f) + curveTo(9.5f, 14.04f, 9.212f, 12.743f, 8.77f, 11.829f) + curveTo(8.31f, 10.881f, 7.77f, 10.5f, 7.333f, 10.5f) + close() + moveTo(4.302f, 10.584f) + curveTo(3.95f, 10.742f, 3.568f, 11.132f, 3.23f, 11.829f) + curveTo(2.788f, 12.743f, 2.5f, 14.04f, 2.5f, 15.5f) + curveTo(2.5f, 16.96f, 2.788f, 18.257f, 3.23f, 19.171f) + curveTo(3.568f, 19.868f, 3.95f, 20.257f, 4.302f, 20.415f) + curveTo(4.31f, 20.216f, 4.436f, 19.99f, 4.701f, 19.929f) + curveTo(4.8f, 19.906f, 4.902f, 19.863f, 5.008f, 19.798f) + curveTo(4.295f, 18.785f, 3.87f, 17.209f, 3.87f, 15.5f) + curveTo(3.87f, 13.791f, 4.294f, 12.215f, 5.007f, 11.201f) + curveTo(4.901f, 11.137f, 4.799f, 11.094f, 4.701f, 11.071f) + curveTo(4.438f, 11.011f, 4.31f, 10.785f, 4.302f, 10.584f) + close() + } + path(fill = SolidColor(cardColor)) { + moveTo(16.2f, 4.998f) + curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f) + curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f) + curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f) + verticalLineTo(14.197f) + curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f) + curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f) + curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f) + horizontalLineTo(10.509f) + curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f) + curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f) + curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f) + horizontalLineTo(4.6f) + curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f) + curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f) + curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f) + curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f) + horizontalLineTo(16.2f) + close() + } + group( + clipPathData = PathData { + moveTo(16.2f, 4.998f) + curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f) + curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f) + curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f) + verticalLineTo(14.197f) + curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f) + curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f) + curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f) + horizontalLineTo(10.509f) + curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f) + curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f) + curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f) + horizontalLineTo(4.6f) + curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f) + curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f) + curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f) + curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f) + horizontalLineTo(16.2f) + close() + }, + ) { + path( + fill = SolidColor(borderColor), + ) { + moveTo(16.2f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(19.362f, 5.324f) + lineTo(19.816f, 4.433f) + lineTo(19.816f, 4.433f) + lineTo(19.362f, 5.324f) + close() + moveTo(20.673f, 6.636f) + lineTo(21.564f, 6.182f) + lineTo(21.564f, 6.182f) + lineTo(20.673f, 6.636f) + close() + moveTo(20.673f, 17.359f) + lineTo(21.564f, 17.813f) + lineTo(21.564f, 17.813f) + lineTo(20.673f, 17.359f) + close() + moveTo(19.362f, 18.671f) + lineTo(19.816f, 19.562f) + lineTo(19.816f, 19.562f) + lineTo(19.362f, 18.671f) + close() + moveTo(16.2f, 18.998f) + verticalLineTo(19.998f) + verticalLineTo(18.998f) + close() + moveTo(10.509f, 18.998f) + lineTo(9.561f, 18.679f) + lineTo(9.116f, 19.998f) + horizontalLineTo(10.509f) + verticalLineTo(18.998f) + close() + moveTo(10.132f, 11.081f) + lineTo(11.028f, 10.638f) + lineTo(11.028f, 10.638f) + lineTo(10.132f, 11.081f) + close() + moveTo(7.488f, 9f) + lineTo(7.488f, 8f) + horizontalLineTo(7.488f) + verticalLineTo(9f) + close() + moveTo(4.6f, 9f) + verticalLineTo(8f) + horizontalLineTo(4.6f) + lineTo(4.6f, 9f) + close() + moveTo(3f, 9.652f) + lineTo(2f, 9.652f) + lineTo(2f, 11.875f) + lineTo(3.663f, 10.401f) + lineTo(3f, 9.652f) + close() + moveTo(3.327f, 6.636f) + lineTo(2.436f, 6.182f) + lineTo(2.436f, 6.182f) + lineTo(3.327f, 6.636f) + close() + moveTo(4.638f, 5.324f) + lineTo(4.184f, 4.433f) + lineTo(4.184f, 4.433f) + lineTo(4.638f, 5.324f) + close() + moveTo(7.8f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(16.2f, 4.998f) + verticalLineTo(5.998f) + curveTo(17.057f, 5.998f, 17.639f, 5.999f, 18.09f, 6.035f) + curveTo(18.528f, 6.071f, 18.752f, 6.136f, 18.908f, 6.215f) + lineTo(19.362f, 5.324f) + lineTo(19.816f, 4.433f) + curveTo(19.331f, 4.186f, 18.814f, 4.087f, 18.252f, 4.042f) + curveTo(17.701f, 3.997f, 17.024f, 3.998f, 16.2f, 3.998f) + verticalLineTo(4.998f) + close() + moveTo(19.362f, 5.324f) + lineTo(18.908f, 6.215f) + curveTo(19.284f, 6.407f, 19.59f, 6.713f, 19.782f, 7.09f) + lineTo(20.673f, 6.636f) + lineTo(21.564f, 6.182f) + curveTo(21.181f, 5.43f, 20.569f, 4.817f, 19.816f, 4.433f) + lineTo(19.362f, 5.324f) + close() + moveTo(20.673f, 6.636f) + lineTo(19.782f, 7.09f) + curveTo(19.861f, 7.246f, 19.927f, 7.47f, 19.962f, 7.909f) + curveTo(19.999f, 8.359f, 20f, 8.941f, 20f, 9.798f) + horizontalLineTo(21f) + horizontalLineTo(22f) + curveTo(22f, 8.974f, 22.001f, 8.296f, 21.956f, 7.746f) + curveTo(21.91f, 7.184f, 21.811f, 6.667f, 21.564f, 6.182f) + lineTo(20.673f, 6.636f) + close() + moveTo(21f, 9.798f) + horizontalLineTo(20f) + verticalLineTo(14.197f) + horizontalLineTo(21f) + horizontalLineTo(22f) + verticalLineTo(9.798f) + horizontalLineTo(21f) + close() + moveTo(21f, 14.197f) + horizontalLineTo(20f) + curveTo(20f, 15.054f, 19.999f, 15.636f, 19.962f, 16.086f) + curveTo(19.927f, 16.525f, 19.861f, 16.749f, 19.782f, 16.905f) + lineTo(20.673f, 17.359f) + lineTo(21.564f, 17.813f) + curveTo(21.811f, 17.328f, 21.91f, 16.811f, 21.956f, 16.249f) + curveTo(22.001f, 15.699f, 22f, 15.021f, 22f, 14.197f) + horizontalLineTo(21f) + close() + moveTo(20.673f, 17.359f) + lineTo(19.782f, 16.905f) + curveTo(19.59f, 17.282f, 19.284f, 17.588f, 18.908f, 17.78f) + lineTo(19.362f, 18.671f) + lineTo(19.816f, 19.562f) + curveTo(20.569f, 19.178f, 21.181f, 18.565f, 21.564f, 17.813f) + lineTo(20.673f, 17.359f) + close() + moveTo(19.362f, 18.671f) + lineTo(18.908f, 17.78f) + curveTo(18.752f, 17.86f, 18.528f, 17.925f, 18.089f, 17.96f) + curveTo(17.639f, 17.997f, 17.057f, 17.998f, 16.2f, 17.998f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + curveTo(17.024f, 19.998f, 17.702f, 19.999f, 18.252f, 19.954f) + curveTo(18.814f, 19.908f, 19.331f, 19.809f, 19.816f, 19.562f) + lineTo(19.362f, 18.671f) + close() + moveTo(16.2f, 18.998f) + verticalLineTo(17.998f) + horizontalLineTo(10.509f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + horizontalLineTo(16.2f) + verticalLineTo(18.998f) + close() + moveTo(10.509f, 18.998f) + lineTo(11.456f, 19.317f) + curveTo(11.837f, 18.188f, 12.044f, 16.874f, 12.044f, 15.5f) + horizontalLineTo(11.044f) + horizontalLineTo(10.044f) + curveTo(10.044f, 16.682f, 9.865f, 17.778f, 9.561f, 18.679f) + lineTo(10.509f, 18.998f) + close() + moveTo(11.044f, 15.5f) + horizontalLineTo(12.044f) + curveTo(12.044f, 13.689f, 11.685f, 11.968f, 11.028f, 10.638f) + lineTo(10.132f, 11.081f) + lineTo(9.235f, 11.524f) + curveTo(9.729f, 12.523f, 10.044f, 13.929f, 10.044f, 15.5f) + horizontalLineTo(11.044f) + close() + moveTo(10.132f, 11.081f) + lineTo(11.028f, 10.638f) + curveTo(10.428f, 9.423f, 9.28f, 8f, 7.488f, 8f) + lineTo(7.488f, 9f) + lineTo(7.488f, 10f) + curveTo(8.095f, 10f, 8.731f, 10.503f, 9.235f, 11.524f) + lineTo(10.132f, 11.081f) + close() + moveTo(7.488f, 9f) + verticalLineTo(8f) + horizontalLineTo(4.6f) + verticalLineTo(9f) + verticalLineTo(10f) + horizontalLineTo(7.488f) + verticalLineTo(9f) + close() + moveTo(4.6f, 9f) + lineTo(4.6f, 8f) + curveTo(3.687f, 8f, 2.925f, 8.382f, 2.337f, 8.904f) + lineTo(3f, 9.652f) + lineTo(3.663f, 10.401f) + curveTo(3.973f, 10.126f, 4.281f, 10f, 4.6f, 10f) + lineTo(4.6f, 9f) + close() + moveTo(3f, 9.652f) + lineTo(4f, 9.653f) + curveTo(4f, 8.847f, 4.003f, 8.297f, 4.041f, 7.871f) + curveTo(4.077f, 7.457f, 4.141f, 7.241f, 4.218f, 7.09f) + lineTo(3.327f, 6.636f) + lineTo(2.436f, 6.182f) + curveTo(2.196f, 6.653f, 2.096f, 7.153f, 2.049f, 7.696f) + curveTo(2.002f, 8.227f, 2f, 8.874f, 2f, 9.652f) + lineTo(3f, 9.652f) + close() + moveTo(3.327f, 6.636f) + lineTo(4.218f, 7.09f) + curveTo(4.41f, 6.713f, 4.716f, 6.407f, 5.092f, 6.215f) + lineTo(4.638f, 5.324f) + lineTo(4.184f, 4.433f) + curveTo(3.43f, 4.817f, 2.819f, 5.43f, 2.436f, 6.182f) + lineTo(3.327f, 6.636f) + close() + moveTo(4.638f, 5.324f) + lineTo(5.092f, 6.215f) + curveTo(5.248f, 6.136f, 5.472f, 6.071f, 5.91f, 6.035f) + curveTo(6.361f, 5.999f, 6.943f, 5.998f, 7.8f, 5.998f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + curveTo(6.977f, 3.998f, 6.299f, 3.997f, 5.748f, 4.042f) + curveTo(5.186f, 4.087f, 4.669f, 4.186f, 4.184f, 4.433f) + lineTo(4.638f, 5.324f) + close() + moveTo(7.8f, 4.998f) + verticalLineTo(5.998f) + horizontalLineTo(16.2f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + horizontalLineTo(7.8f) + verticalLineTo(4.998f) + close() + } + } + }.build() + + fun buildRingWithCard2( + mainColor: Color, + cardColor: Color, + secondCardColor: Color, + borderColor: Color, + ): ImageVector = ImageVector.Builder( + name = "RingTwoCards", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(3.667f, 10f) + curveTo(2.194f, 10f, 1f, 12.462f, 1f, 15.5f) + curveTo(1f, 18.538f, 2.194f, 21f, 3.667f, 21f) + horizontalLineTo(6.333f) + curveTo(7.806f, 21f, 9f, 18.538f, 9f, 15.5f) + curveTo(9f, 12.462f, 7.806f, 10f, 6.333f, 10f) + horizontalLineTo(3.667f) + close() + moveTo(3.815f, 10.55f) + curveTo(7.402f, 10.55f, 7.46f, 20.45f, 3.815f, 20.45f) + curveTo(3.795f, 20.45f, 3.793f, 20.421f, 3.813f, 20.416f) + curveTo(4.099f, 20.351f, 4.371f, 20.19f, 4.62f, 19.952f) + curveTo(4.668f, 19.906f, 4.668f, 19.83f, 4.624f, 19.781f) + curveTo(3.874f, 18.923f, 3.37f, 17.327f, 3.37f, 15.5f) + curveTo(3.37f, 13.672f, 3.875f, 12.076f, 4.624f, 11.219f) + curveTo(4.668f, 11.169f, 4.668f, 11.093f, 4.62f, 11.047f) + curveTo(4.371f, 10.809f, 4.1f, 10.649f, 3.813f, 10.584f) + curveTo(3.793f, 10.579f, 3.795f, 10.55f, 3.815f, 10.55f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(6.333f, 10.5f) + horizontalLineTo(5.192f) + curveTo(5.657f, 10.835f, 6.021f, 11.328f, 6.29f, 11.878f) + curveTo(6.785f, 12.893f, 7.023f, 14.21f, 7.027f, 15.498f) + curveTo(7.031f, 16.786f, 6.801f, 18.106f, 6.307f, 19.122f) + curveTo(6.038f, 19.674f, 5.674f, 20.166f, 5.207f, 20.5f) + horizontalLineTo(6.333f) + curveTo(6.77f, 20.5f, 7.31f, 20.119f, 7.77f, 19.171f) + curveTo(8.212f, 18.257f, 8.5f, 16.96f, 8.5f, 15.5f) + curveTo(8.5f, 14.04f, 8.212f, 12.743f, 7.77f, 11.829f) + curveTo(7.31f, 10.881f, 6.77f, 10.5f, 6.333f, 10.5f) + close() + moveTo(3.302f, 10.584f) + curveTo(2.95f, 10.742f, 2.568f, 11.132f, 2.23f, 11.829f) + curveTo(1.788f, 12.743f, 1.5f, 14.04f, 1.5f, 15.5f) + curveTo(1.5f, 16.96f, 1.788f, 18.257f, 2.23f, 19.171f) + curveTo(2.568f, 19.868f, 2.95f, 20.257f, 3.302f, 20.415f) + curveTo(3.31f, 20.216f, 3.436f, 19.99f, 3.701f, 19.929f) + curveTo(3.8f, 19.906f, 3.902f, 19.863f, 4.008f, 19.798f) + curveTo(3.295f, 18.785f, 2.87f, 17.209f, 2.87f, 15.5f) + curveTo(2.87f, 13.791f, 3.294f, 12.215f, 4.007f, 11.201f) + curveTo(3.901f, 11.137f, 3.799f, 11.094f, 3.701f, 11.071f) + curveTo(3.438f, 11.011f, 3.31f, 10.785f, 3.302f, 10.584f) + close() + } + path(fill = SolidColor(cardColor)) { + moveTo(15.2f, 4.998f) + curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f) + curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f) + curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f) + verticalLineTo(14.197f) + curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f) + curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f) + curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f) + horizontalLineTo(9.509f) + curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f) + curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f) + curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f) + horizontalLineTo(3.6f) + curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f) + curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f) + curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f) + curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f) + horizontalLineTo(15.2f) + close() + } + group( + clipPathData = PathData { + moveTo(15.2f, 4.998f) + curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f) + curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f) + curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f) + verticalLineTo(14.197f) + curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f) + curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f) + curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f) + horizontalLineTo(9.509f) + curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f) + curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f) + curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f) + horizontalLineTo(3.6f) + curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f) + curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f) + curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f) + curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f) + horizontalLineTo(15.2f) + close() + }, + ) { + path( + fill = SolidColor(secondCardColor), + fillAlpha = 0.1f, + ) { + moveTo(15.2f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(18.362f, 5.324f) + lineTo(18.816f, 4.433f) + lineTo(18.816f, 4.433f) + lineTo(18.362f, 5.324f) + close() + moveTo(19.673f, 6.636f) + lineTo(20.564f, 6.182f) + lineTo(20.564f, 6.182f) + lineTo(19.673f, 6.636f) + close() + moveTo(19.673f, 17.359f) + lineTo(20.564f, 17.813f) + lineTo(20.564f, 17.813f) + lineTo(19.673f, 17.359f) + close() + moveTo(18.362f, 18.671f) + lineTo(18.816f, 19.562f) + lineTo(18.816f, 19.562f) + lineTo(18.362f, 18.671f) + close() + moveTo(15.2f, 18.998f) + verticalLineTo(19.998f) + verticalLineTo(18.998f) + close() + moveTo(9.509f, 18.998f) + lineTo(8.561f, 18.679f) + lineTo(8.116f, 19.998f) + horizontalLineTo(9.509f) + verticalLineTo(18.998f) + close() + moveTo(9.132f, 11.081f) + lineTo(10.028f, 10.638f) + lineTo(10.028f, 10.638f) + lineTo(9.132f, 11.081f) + close() + moveTo(6.488f, 9f) + lineTo(6.488f, 8f) + horizontalLineTo(6.488f) + verticalLineTo(9f) + close() + moveTo(3.6f, 9f) + verticalLineTo(8f) + horizontalLineTo(3.6f) + lineTo(3.6f, 9f) + close() + moveTo(2f, 9.652f) + lineTo(1f, 9.652f) + lineTo(1f, 11.875f) + lineTo(2.663f, 10.401f) + lineTo(2f, 9.652f) + close() + moveTo(2.327f, 6.636f) + lineTo(1.436f, 6.182f) + lineTo(1.436f, 6.182f) + lineTo(2.327f, 6.636f) + close() + moveTo(3.638f, 5.324f) + lineTo(3.184f, 4.433f) + lineTo(3.184f, 4.433f) + lineTo(3.638f, 5.324f) + close() + moveTo(6.8f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(15.2f, 4.998f) + verticalLineTo(5.998f) + curveTo(16.057f, 5.998f, 16.639f, 5.999f, 17.09f, 6.035f) + curveTo(17.528f, 6.071f, 17.752f, 6.136f, 17.908f, 6.215f) + lineTo(18.362f, 5.324f) + lineTo(18.816f, 4.433f) + curveTo(18.331f, 4.186f, 17.814f, 4.087f, 17.252f, 4.042f) + curveTo(16.701f, 3.997f, 16.024f, 3.998f, 15.2f, 3.998f) + verticalLineTo(4.998f) + close() + moveTo(18.362f, 5.324f) + lineTo(17.908f, 6.215f) + curveTo(18.284f, 6.407f, 18.59f, 6.713f, 18.782f, 7.09f) + lineTo(19.673f, 6.636f) + lineTo(20.564f, 6.182f) + curveTo(20.181f, 5.43f, 19.569f, 4.817f, 18.816f, 4.433f) + lineTo(18.362f, 5.324f) + close() + moveTo(19.673f, 6.636f) + lineTo(18.782f, 7.09f) + curveTo(18.861f, 7.246f, 18.927f, 7.47f, 18.962f, 7.909f) + curveTo(18.999f, 8.359f, 19f, 8.941f, 19f, 9.798f) + horizontalLineTo(20f) + horizontalLineTo(21f) + curveTo(21f, 8.974f, 21.001f, 8.296f, 20.956f, 7.746f) + curveTo(20.91f, 7.184f, 20.811f, 6.667f, 20.564f, 6.182f) + lineTo(19.673f, 6.636f) + close() + moveTo(20f, 9.798f) + horizontalLineTo(19f) + verticalLineTo(14.197f) + horizontalLineTo(20f) + horizontalLineTo(21f) + verticalLineTo(9.798f) + horizontalLineTo(20f) + close() + moveTo(20f, 14.197f) + horizontalLineTo(19f) + curveTo(19f, 15.054f, 18.999f, 15.636f, 18.962f, 16.086f) + curveTo(18.927f, 16.525f, 18.861f, 16.749f, 18.782f, 16.905f) + lineTo(19.673f, 17.359f) + lineTo(20.564f, 17.813f) + curveTo(20.811f, 17.328f, 20.91f, 16.811f, 20.956f, 16.249f) + curveTo(21.001f, 15.699f, 21f, 15.021f, 21f, 14.197f) + horizontalLineTo(20f) + close() + moveTo(19.673f, 17.359f) + lineTo(18.782f, 16.905f) + curveTo(18.59f, 17.282f, 18.284f, 17.588f, 17.908f, 17.78f) + lineTo(18.362f, 18.671f) + lineTo(18.816f, 19.562f) + curveTo(19.569f, 19.178f, 20.181f, 18.565f, 20.564f, 17.813f) + lineTo(19.673f, 17.359f) + close() + moveTo(18.362f, 18.671f) + lineTo(17.908f, 17.78f) + curveTo(17.752f, 17.86f, 17.528f, 17.925f, 17.089f, 17.96f) + curveTo(16.639f, 17.997f, 16.057f, 17.998f, 15.2f, 17.998f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + curveTo(16.024f, 19.998f, 16.702f, 19.999f, 17.252f, 19.954f) + curveTo(17.814f, 19.908f, 18.331f, 19.809f, 18.816f, 19.562f) + lineTo(18.362f, 18.671f) + close() + moveTo(15.2f, 18.998f) + verticalLineTo(17.998f) + horizontalLineTo(9.509f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + horizontalLineTo(15.2f) + verticalLineTo(18.998f) + close() + moveTo(9.509f, 18.998f) + lineTo(10.456f, 19.317f) + curveTo(10.837f, 18.188f, 11.044f, 16.874f, 11.044f, 15.5f) + horizontalLineTo(10.044f) + horizontalLineTo(9.044f) + curveTo(9.044f, 16.682f, 8.865f, 17.778f, 8.561f, 18.679f) + lineTo(9.509f, 18.998f) + close() + moveTo(10.044f, 15.5f) + horizontalLineTo(11.044f) + curveTo(11.044f, 13.689f, 10.685f, 11.968f, 10.028f, 10.638f) + lineTo(9.132f, 11.081f) + lineTo(8.235f, 11.524f) + curveTo(8.729f, 12.523f, 9.044f, 13.929f, 9.044f, 15.5f) + horizontalLineTo(10.044f) + close() + moveTo(9.132f, 11.081f) + lineTo(10.028f, 10.638f) + curveTo(9.428f, 9.423f, 8.28f, 8f, 6.488f, 8f) + lineTo(6.488f, 9f) + lineTo(6.488f, 10f) + curveTo(7.095f, 10f, 7.731f, 10.503f, 8.235f, 11.524f) + lineTo(9.132f, 11.081f) + close() + moveTo(6.488f, 9f) + verticalLineTo(8f) + horizontalLineTo(3.6f) + verticalLineTo(9f) + verticalLineTo(10f) + horizontalLineTo(6.488f) + verticalLineTo(9f) + close() + moveTo(3.6f, 9f) + lineTo(3.6f, 8f) + curveTo(2.687f, 8f, 1.925f, 8.382f, 1.337f, 8.904f) + lineTo(2f, 9.652f) + lineTo(2.663f, 10.401f) + curveTo(2.973f, 10.126f, 3.281f, 10f, 3.6f, 10f) + lineTo(3.6f, 9f) + close() + moveTo(2f, 9.652f) + lineTo(3f, 9.653f) + curveTo(3f, 8.847f, 3.003f, 8.297f, 3.041f, 7.871f) + curveTo(3.077f, 7.457f, 3.141f, 7.241f, 3.218f, 7.09f) + lineTo(2.327f, 6.636f) + lineTo(1.436f, 6.182f) + curveTo(1.196f, 6.653f, 1.096f, 7.153f, 1.048f, 7.696f) + curveTo(1.002f, 8.227f, 1f, 8.874f, 1f, 9.652f) + lineTo(2f, 9.652f) + close() + moveTo(2.327f, 6.636f) + lineTo(3.218f, 7.09f) + curveTo(3.41f, 6.713f, 3.716f, 6.407f, 4.092f, 6.215f) + lineTo(3.638f, 5.324f) + lineTo(3.184f, 4.433f) + curveTo(2.43f, 4.817f, 1.819f, 5.43f, 1.436f, 6.182f) + lineTo(2.327f, 6.636f) + close() + moveTo(3.638f, 5.324f) + lineTo(4.092f, 6.215f) + curveTo(4.248f, 6.136f, 4.472f, 6.071f, 4.91f, 6.035f) + curveTo(5.361f, 5.999f, 5.943f, 5.998f, 6.8f, 5.998f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + curveTo(5.977f, 3.998f, 5.299f, 3.997f, 4.748f, 4.042f) + curveTo(4.186f, 4.087f, 3.669f, 4.186f, 3.184f, 4.433f) + lineTo(3.638f, 5.324f) + close() + moveTo(6.8f, 4.998f) + verticalLineTo(5.998f) + horizontalLineTo(15.2f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + horizontalLineTo(6.8f) + verticalLineTo(4.998f) + close() + } + } + path(fill = SolidColor(secondCardColor)) { + moveTo(18.035f, 5f) + curveTo(19.772f, 5f, 20.642f, 5f, 21.306f, 5.327f) + curveTo(21.889f, 5.615f, 22.364f, 6.073f, 22.662f, 6.638f) + curveTo(23f, 7.279f, 23f, 8.12f, 23f, 9.8f) + verticalLineTo(14.2f) + curveTo(23f, 15.88f, 23f, 16.721f, 22.662f, 17.362f) + curveTo(22.364f, 17.927f, 21.889f, 18.385f, 21.306f, 18.673f) + curveTo(20.642f, 19f, 19.772f, 19f, 18.035f, 19f) + horizontalLineTo(17f) + curveTo(18.738f, 19f, 19.607f, 19f, 20.271f, 18.673f) + curveTo(20.855f, 18.385f, 21.33f, 17.927f, 21.627f, 17.362f) + curveTo(21.965f, 16.721f, 21.965f, 15.88f, 21.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(21.965f, 8.12f, 21.965f, 7.279f, 21.627f, 6.638f) + curveTo(21.33f, 6.073f, 20.855f, 5.615f, 20.271f, 5.327f) + curveTo(19.607f, 5f, 18.738f, 5f, 17f, 5f) + horizontalLineTo(18.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(22.331f, 7.153f) + curveTo(22.39f, 7.342f, 22.434f, 7.57f, 22.459f, 7.871f) + curveTo(22.499f, 8.345f, 22.5f, 8.951f, 22.5f, 9.8f) + verticalLineTo(14.2f) + lineTo(22.495f, 15.305f) + curveTo(22.49f, 15.621f, 22.479f, 15.892f, 22.459f, 16.129f) + curveTo(22.434f, 16.429f, 22.39f, 16.657f, 22.331f, 16.846f) + curveTo(22.373f, 16.648f, 22.402f, 16.438f, 22.421f, 16.213f) + curveTo(22.465f, 15.687f, 22.466f, 15.032f, 22.466f, 14.2f) + verticalLineTo(9.8f) + lineTo(22.46f, 8.679f) + curveTo(22.455f, 8.345f, 22.443f, 8.05f, 22.421f, 7.787f) + curveTo(22.402f, 7.561f, 22.373f, 7.351f, 22.331f, 7.153f) + close() + } + }.build() + + fun buildCard(mainColor: Color, borderColor: Color, tColor: Color?): ImageVector = ImageVector.Builder( + name = "KeyCard", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(3f, 9.8f) + curveTo(3f, 8.12f, 3f, 7.28f, 3.327f, 6.638f) + curveTo(3.615f, 6.074f, 4.074f, 5.615f, 4.638f, 5.327f) + curveTo(5.28f, 5f, 6.12f, 5f, 7.8f, 5f) + horizontalLineTo(16.2f) + curveTo(17.88f, 5f, 18.72f, 5f, 19.362f, 5.327f) + curveTo(19.927f, 5.615f, 20.385f, 6.074f, 20.673f, 6.638f) + curveTo(21f, 7.28f, 21f, 8.12f, 21f, 9.8f) + verticalLineTo(14.2f) + curveTo(21f, 15.88f, 21f, 16.72f, 20.673f, 17.362f) + curveTo(20.385f, 17.927f, 19.927f, 18.385f, 19.362f, 18.673f) + curveTo(18.72f, 19f, 17.88f, 19f, 16.2f, 19f) + horizontalLineTo(7.8f) + curveTo(6.12f, 19f, 5.28f, 19f, 4.638f, 18.673f) + curveTo(4.074f, 18.385f, 3.615f, 17.927f, 3.327f, 17.362f) + curveTo(3f, 16.72f, 3f, 15.88f, 3f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(7.8f, 5.5f) + horizontalLineTo(16.2f) + curveTo(17.048f, 5.5f, 17.655f, 5.5f, 18.13f, 5.539f) + curveTo(18.599f, 5.577f, 18.896f, 5.651f, 19.135f, 5.772f) + curveTo(19.605f, 6.012f, 19.988f, 6.395f, 20.228f, 6.865f) + curveTo(20.349f, 7.104f, 20.423f, 7.401f, 20.461f, 7.87f) + curveTo(20.5f, 8.345f, 20.5f, 8.952f, 20.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(20.5f, 15.048f, 20.5f, 15.655f, 20.461f, 16.13f) + curveTo(20.423f, 16.599f, 20.349f, 16.896f, 20.228f, 17.135f) + curveTo(19.988f, 17.605f, 19.605f, 17.988f, 19.135f, 18.228f) + curveTo(18.896f, 18.349f, 18.599f, 18.423f, 18.13f, 18.461f) + curveTo(17.655f, 18.5f, 17.048f, 18.5f, 16.2f, 18.5f) + horizontalLineTo(7.8f) + curveTo(6.952f, 18.5f, 6.345f, 18.5f, 5.87f, 18.461f) + curveTo(5.401f, 18.423f, 5.104f, 18.349f, 4.865f, 18.228f) + curveTo(4.395f, 17.988f, 4.012f, 17.605f, 3.772f, 17.135f) + curveTo(3.651f, 16.896f, 3.577f, 16.599f, 3.539f, 16.13f) + curveTo(3.5f, 15.655f, 3.5f, 15.048f, 3.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(3.5f, 8.952f, 3.5f, 8.345f, 3.539f, 7.87f) + curveTo(3.577f, 7.401f, 3.651f, 7.104f, 3.772f, 6.865f) + curveTo(4.012f, 6.395f, 4.395f, 6.012f, 4.865f, 5.772f) + curveTo(5.104f, 5.651f, 5.401f, 5.577f, 5.87f, 5.539f) + curveTo(6.345f, 5.5f, 6.952f, 5.5f, 7.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(11.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(9f) + verticalLineTo(8f) + horizontalLineTo(15f) + verticalLineTo(9.635f) + horizontalLineTo(12.913f) + verticalLineTo(16f) + horizontalLineTo(11.082f) + close() + } + } + }.build() + + fun buildCard2(mainColor: Color, secondColor: Color, borderColor: Color, tColor: Color?): ImageVector = + ImageVector.Builder( + name = "KeyCard2", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(2f, 9.8f) + curveTo(2f, 8.12f, 2f, 7.28f, 2.327f, 6.638f) + curveTo(2.615f, 6.074f, 3.074f, 5.615f, 3.638f, 5.327f) + curveTo(4.28f, 5f, 5.12f, 5f, 6.8f, 5f) + horizontalLineTo(15.2f) + curveTo(16.88f, 5f, 17.72f, 5f, 18.362f, 5.327f) + curveTo(18.927f, 5.615f, 19.385f, 6.074f, 19.673f, 6.638f) + curveTo(20f, 7.28f, 20f, 8.12f, 20f, 9.8f) + verticalLineTo(14.2f) + curveTo(20f, 15.88f, 20f, 16.72f, 19.673f, 17.362f) + curveTo(19.385f, 17.927f, 18.927f, 18.385f, 18.362f, 18.673f) + curveTo(17.72f, 19f, 16.88f, 19f, 15.2f, 19f) + horizontalLineTo(6.8f) + curveTo(5.12f, 19f, 4.28f, 19f, 3.638f, 18.673f) + curveTo(3.074f, 18.385f, 2.615f, 17.927f, 2.327f, 17.362f) + curveTo(2f, 16.72f, 2f, 15.88f, 2f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(6.8f, 5.5f) + horizontalLineTo(15.2f) + curveTo(16.048f, 5.5f, 16.655f, 5.5f, 17.13f, 5.539f) + curveTo(17.599f, 5.577f, 17.896f, 5.651f, 18.135f, 5.772f) + curveTo(18.605f, 6.012f, 18.988f, 6.395f, 19.228f, 6.865f) + curveTo(19.349f, 7.104f, 19.423f, 7.401f, 19.461f, 7.87f) + curveTo(19.5f, 8.345f, 19.5f, 8.952f, 19.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(19.5f, 15.048f, 19.5f, 15.655f, 19.461f, 16.13f) + curveTo(19.423f, 16.599f, 19.349f, 16.896f, 19.228f, 17.135f) + curveTo(18.988f, 17.605f, 18.605f, 17.988f, 18.135f, 18.228f) + curveTo(17.896f, 18.349f, 17.599f, 18.423f, 17.13f, 18.461f) + curveTo(16.655f, 18.5f, 16.048f, 18.5f, 15.2f, 18.5f) + horizontalLineTo(6.8f) + curveTo(5.952f, 18.5f, 5.345f, 18.5f, 4.87f, 18.461f) + curveTo(4.401f, 18.423f, 4.104f, 18.349f, 3.865f, 18.228f) + curveTo(3.395f, 17.988f, 3.012f, 17.605f, 2.772f, 17.135f) + curveTo(2.651f, 16.896f, 2.577f, 16.599f, 2.539f, 16.13f) + curveTo(2.5f, 15.655f, 2.5f, 15.048f, 2.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(2.5f, 8.952f, 2.5f, 8.345f, 2.539f, 7.87f) + curveTo(2.577f, 7.401f, 2.651f, 7.104f, 2.772f, 6.865f) + curveTo(3.012f, 6.395f, 3.395f, 6.012f, 3.865f, 5.772f) + curveTo(4.104f, 5.651f, 4.401f, 5.577f, 4.87f, 5.539f) + curveTo(5.345f, 5.5f, 5.952f, 5.5f, 6.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(10.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(8f) + verticalLineTo(8f) + horizontalLineTo(14f) + verticalLineTo(9.635f) + horizontalLineTo(11.913f) + verticalLineTo(16f) + horizontalLineTo(10.082f) + close() + } + } + path(fill = SolidColor(secondColor)) { + moveTo(17.535f, 5f) + curveTo(19.272f, 5f, 20.142f, 5f, 20.806f, 5.327f) + curveTo(21.389f, 5.615f, 21.864f, 6.073f, 22.162f, 6.638f) + curveTo(22.5f, 7.279f, 22.5f, 8.12f, 22.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(22.5f, 15.88f, 22.5f, 16.721f, 22.162f, 17.362f) + curveTo(21.864f, 17.927f, 21.389f, 18.385f, 20.806f, 18.673f) + curveTo(20.142f, 19f, 19.272f, 19f, 17.535f, 19f) + horizontalLineTo(16.5f) + curveTo(18.238f, 19f, 19.107f, 19f, 19.771f, 18.673f) + curveTo(20.355f, 18.385f, 20.83f, 17.927f, 21.127f, 17.362f) + curveTo(21.465f, 16.721f, 21.465f, 15.88f, 21.465f, 14.2f) + verticalLineTo(9.8f) + curveTo(21.465f, 8.12f, 21.465f, 7.279f, 21.127f, 6.638f) + curveTo(20.83f, 6.073f, 20.355f, 5.615f, 19.771f, 5.327f) + curveTo(19.107f, 5f, 18.238f, 5f, 16.5f, 5f) + horizontalLineTo(17.535f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(21.831f, 7.153f) + curveTo(21.89f, 7.342f, 21.934f, 7.57f, 21.959f, 7.871f) + curveTo(21.999f, 8.345f, 22f, 8.951f, 22f, 9.8f) + verticalLineTo(14.2f) + lineTo(21.995f, 15.305f) + curveTo(21.99f, 15.621f, 21.979f, 15.892f, 21.959f, 16.129f) + curveTo(21.934f, 16.429f, 21.89f, 16.657f, 21.831f, 16.846f) + curveTo(21.873f, 16.648f, 21.902f, 16.438f, 21.921f, 16.213f) + curveTo(21.965f, 15.687f, 21.966f, 15.032f, 21.966f, 14.2f) + verticalLineTo(9.8f) + lineTo(21.96f, 8.679f) + curveTo(21.955f, 8.345f, 21.943f, 8.05f, 21.921f, 7.787f) + curveTo(21.902f, 7.561f, 21.873f, 7.351f, 21.831f, 7.153f) + close() + } + }.build() + + fun buildCard3( + mainColor: Color, + secondColor: Color, + thirdColor: Color, + borderColor: Color, + tColor: Color?, + ): ImageVector = ImageVector.Builder( + name = "KeyCard3", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(0f, 9.8f) + curveTo(0f, 8.12f, 0f, 7.28f, 0.327f, 6.638f) + curveTo(0.615f, 6.074f, 1.074f, 5.615f, 1.638f, 5.327f) + curveTo(2.28f, 5f, 3.12f, 5f, 4.8f, 5f) + horizontalLineTo(13.2f) + curveTo(14.88f, 5f, 15.72f, 5f, 16.362f, 5.327f) + curveTo(16.927f, 5.615f, 17.385f, 6.074f, 17.673f, 6.638f) + curveTo(18f, 7.28f, 18f, 8.12f, 18f, 9.8f) + verticalLineTo(14.2f) + curveTo(18f, 15.88f, 18f, 16.72f, 17.673f, 17.362f) + curveTo(17.385f, 17.927f, 16.927f, 18.385f, 16.362f, 18.673f) + curveTo(15.72f, 19f, 14.88f, 19f, 13.2f, 19f) + horizontalLineTo(4.8f) + curveTo(3.12f, 19f, 2.28f, 19f, 1.638f, 18.673f) + curveTo(1.074f, 18.385f, 0.615f, 17.927f, 0.327f, 17.362f) + curveTo(0f, 16.72f, 0f, 15.88f, 0f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(4.8f, 5.5f) + horizontalLineTo(13.2f) + curveTo(14.048f, 5.5f, 14.655f, 5.5f, 15.13f, 5.539f) + curveTo(15.599f, 5.577f, 15.896f, 5.651f, 16.135f, 5.772f) + curveTo(16.605f, 6.012f, 16.988f, 6.395f, 17.228f, 6.865f) + curveTo(17.349f, 7.104f, 17.423f, 7.401f, 17.461f, 7.87f) + curveTo(17.5f, 8.345f, 17.5f, 8.952f, 17.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(17.5f, 15.048f, 17.5f, 15.655f, 17.461f, 16.13f) + curveTo(17.423f, 16.599f, 17.349f, 16.896f, 17.228f, 17.135f) + curveTo(16.988f, 17.605f, 16.605f, 17.988f, 16.135f, 18.228f) + curveTo(15.896f, 18.349f, 15.599f, 18.423f, 15.13f, 18.461f) + curveTo(14.655f, 18.5f, 14.048f, 18.5f, 13.2f, 18.5f) + horizontalLineTo(4.8f) + curveTo(3.952f, 18.5f, 3.345f, 18.5f, 2.87f, 18.461f) + curveTo(2.401f, 18.423f, 2.104f, 18.349f, 1.865f, 18.228f) + curveTo(1.395f, 17.988f, 1.012f, 17.605f, 0.772f, 17.135f) + curveTo(0.651f, 16.896f, 0.577f, 16.599f, 0.539f, 16.13f) + curveTo(0.5f, 15.655f, 0.5f, 15.048f, 0.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(0.5f, 8.952f, 0.5f, 8.345f, 0.539f, 7.87f) + curveTo(0.577f, 7.401f, 0.651f, 7.104f, 0.772f, 6.865f) + curveTo(1.012f, 6.395f, 1.395f, 6.012f, 1.865f, 5.772f) + curveTo(2.104f, 5.651f, 2.401f, 5.577f, 2.87f, 5.539f) + curveTo(3.345f, 5.5f, 3.952f, 5.5f, 4.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(8.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(6f) + verticalLineTo(8f) + horizontalLineTo(12f) + verticalLineTo(9.635f) + horizontalLineTo(9.913f) + verticalLineTo(16f) + horizontalLineTo(8.082f) + close() + } + } + path(fill = SolidColor(secondColor)) { + moveTo(16.035f, 5f) + curveTo(17.772f, 5f, 18.642f, 5f, 19.306f, 5.327f) + curveTo(19.889f, 5.615f, 20.364f, 6.073f, 20.662f, 6.638f) + curveTo(21f, 7.279f, 21f, 8.12f, 21f, 9.8f) + verticalLineTo(14.2f) + curveTo(21f, 15.88f, 21f, 16.721f, 20.662f, 17.362f) + curveTo(20.364f, 17.927f, 19.889f, 18.385f, 19.306f, 18.673f) + curveTo(18.642f, 19f, 17.772f, 19f, 16.035f, 19f) + horizontalLineTo(15f) + curveTo(16.738f, 19f, 17.607f, 19f, 18.271f, 18.673f) + curveTo(18.855f, 18.385f, 19.33f, 17.927f, 19.627f, 17.362f) + curveTo(19.965f, 16.721f, 19.965f, 15.88f, 19.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(19.965f, 8.12f, 19.965f, 7.279f, 19.627f, 6.638f) + curveTo(19.33f, 6.073f, 18.855f, 5.615f, 18.271f, 5.327f) + curveTo(17.607f, 5f, 16.738f, 5f, 15f, 5f) + horizontalLineTo(16.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(20.331f, 7.153f) + curveTo(20.39f, 7.342f, 20.434f, 7.57f, 20.459f, 7.871f) + curveTo(20.499f, 8.345f, 20.5f, 8.951f, 20.5f, 9.8f) + verticalLineTo(14.2f) + lineTo(20.495f, 15.305f) + curveTo(20.49f, 15.621f, 20.479f, 15.892f, 20.459f, 16.129f) + curveTo(20.434f, 16.429f, 20.39f, 16.657f, 20.331f, 16.846f) + curveTo(20.373f, 16.648f, 20.402f, 16.438f, 20.421f, 16.213f) + curveTo(20.465f, 15.687f, 20.466f, 15.032f, 20.466f, 14.2f) + verticalLineTo(9.8f) + lineTo(20.46f, 8.679f) + curveTo(20.455f, 8.345f, 20.443f, 8.05f, 20.421f, 7.787f) + curveTo(20.402f, 7.561f, 20.373f, 7.351f, 20.331f, 7.153f) + close() + } + path(fill = SolidColor(thirdColor)) { + moveTo(19.035f, 5f) + curveTo(20.772f, 5f, 21.642f, 5f, 22.306f, 5.327f) + curveTo(22.889f, 5.615f, 23.364f, 6.073f, 23.662f, 6.638f) + curveTo(24f, 7.279f, 24f, 8.12f, 24f, 9.8f) + verticalLineTo(14.2f) + curveTo(24f, 15.88f, 24f, 16.721f, 23.662f, 17.362f) + curveTo(23.364f, 17.927f, 22.889f, 18.385f, 22.306f, 18.673f) + curveTo(21.642f, 19f, 20.772f, 19f, 19.035f, 19f) + horizontalLineTo(18f) + curveTo(19.738f, 19f, 20.607f, 19f, 21.271f, 18.673f) + curveTo(21.855f, 18.385f, 22.33f, 17.927f, 22.627f, 17.362f) + curveTo(22.965f, 16.721f, 22.965f, 15.88f, 22.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(22.965f, 8.12f, 22.965f, 7.279f, 22.627f, 6.638f) + curveTo(22.33f, 6.073f, 21.855f, 5.615f, 21.271f, 5.327f) + curveTo(20.607f, 5f, 19.738f, 5f, 18f, 5f) + horizontalLineTo(19.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(23.331f, 7.153f) + curveTo(23.39f, 7.342f, 23.434f, 7.57f, 23.459f, 7.871f) + curveTo(23.499f, 8.345f, 23.5f, 8.951f, 23.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(23.5f, 15.049f, 23.499f, 15.655f, 23.459f, 16.129f) + curveTo(23.434f, 16.429f, 23.39f, 16.657f, 23.331f, 16.846f) + curveTo(23.373f, 16.648f, 23.402f, 16.438f, 23.421f, 16.213f) + curveTo(23.465f, 15.687f, 23.466f, 15.032f, 23.466f, 14.2f) + verticalLineTo(9.8f) + curveTo(23.466f, 8.968f, 23.465f, 8.313f, 23.421f, 7.787f) + curveTo(23.402f, 7.561f, 23.373f, 7.351f, 23.331f, 7.153f) + close() + } + }.build() +} \ 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 3591920330..86d955f2d9 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 @@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R -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.ds.image.TangemIconUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -58,7 +58,15 @@ fun TangemMessage( if (messageUM.iconUM != null) { TangemIcon( tangemIconUM = messageUM.iconUM, - modifier = Modifier.size(TangemTheme.dimens2.x8), + modifier = Modifier + .align( + if (messageUM.buttonsUM.isEmpty()) { + Alignment.CenterVertically + } else { + Alignment.Top + }, + ) + .size(TangemTheme.dimens2.x7), ) } }, @@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider if (isInDarkTheme) { - persistentListOf() + persistentListOf( + Color(0x1AFFFFFF), + Color(0x1AFFFFFF), + ) } else { persistentListOf( Color(0x0d000000), @@ -238,9 +244,10 @@ internal fun Modifier.messageEffectBackground( val isInDarkTheme = LocalIsInDarkTheme.current val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) } val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) } + val gradientTint = remember { messageEffect.getGradientTint(isInDarkTheme) } val angle by rememberAnimationAngle(messageEffect.isAnimatable) - val brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) + val brush = remember { Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) } val padding = 1.dp.toPx() return this @@ -254,14 +261,14 @@ internal fun Modifier.messageEffectBackground( border( width = 1.dp, brush = Brush.sweepGradient( - colors = messageEffect.getBorderGradient(isInDarkTheme), + colors = borderGradientColors, center = Offset.Infinite, ), shape = RoundedCornerShape(radius), ) } .hazeForegroundEffectTangem( - style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)), + style = HazeStyle(tints = gradientTint), isBlurEnabled = true, ) { fallbackTint = HazeTint( diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt new file mode 100644 index 0000000000..8fd7342071 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -0,0 +1,274 @@ +package com.tangem.core.ui.ds.opportunities + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import dev.chrisbanes.haze.HazeStyle + +/** + * Container that draws a blurred background (from URL or solid color) and + * applies a semi‑transparent overlay on top of it, then renders foreground content. + * + * Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev + * + * @param icon Background configuration (URL, solid color or none). + * @param modifier Modifier applied to the outer container. + * @param content Foreground content rendered on top of the overlay. + * @param shape Shape used for inner shadow and border (e.g. rounded corners). + */ +@Suppress("MagicNumber") +@Composable +fun OpportunitiesBG( + icon: TangemIconUM, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(16.dp), + content: @Composable BoxScope.() -> Unit, +) { + val isInDarkTheme = LocalIsInDarkTheme.current + val overlayColor = remember(isInDarkTheme) { + if (isInDarkTheme) { + Color(OVERLAY_DARK) + } else { + Color.White + } + } + + Box(modifier = modifier) { + BackgroundLayer(icon = icon) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 30.dp, + spread = 5.dp, + color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f), + offset = DpOffset(0.dp, 0.dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 100.dp, + spread = (-39).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(.3f), + offset = DpOffset(0.dp, (-56).dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 40.dp, + spread = (-19).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f), + offset = DpOffset(0.dp, (-16).dp), + ), + ) + .drawWithContent { + drawRect(color = overlayColor.copy(alpha = .7f)) + drawContent() + val outline = shape.createOutline(size, layoutDirection, this) + drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx())) + }, + content = content, + ) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) { + when (icon) { + is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius) + is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius) + is TangemIconUM.Ident -> Unit + is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius) + is TangemIconUM.Url -> UrlColorBackground(icon.url, blurRadius) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) { + when (state) { + is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CustomTokenIcon -> SolidColorBackground( + color = state.background, + blurRadius = blurRadius, + ) + is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius) + is CurrencyIconState.CoinIcon -> { + state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + } + is CurrencyIconState.FiatIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + is CurrencyIconState.TokenIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + SolidColorBackground( + color = state.fallbackBackground, + blurRadius = blurRadius, + ) + } + CurrencyIconState.Loading -> Unit + CurrencyIconState.Locked -> Unit + } +} + +@Composable +private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { + val context = LocalContext.current + + val imageRequest = remember(imageUrl) { + if (imageUrl.isNullOrBlank()) { + null + } else { + ImageRequest.Builder(context) + .data(imageUrl) + .crossfade(true) + .build() + } + } + + if (imageRequest != null) { + AsyncImage( + model = imageRequest, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) + } +} + +@Composable +private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { + Image( + painter = painterResource(res), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +@Composable +private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = color) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +@Composable +private fun BoxScope.UrlColorBackground(url: String, blurRadius: Dp) { + SubcomposeAsyncImage( + modifier = Modifier + .matchParentSize() + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + model = ImageRequest.Builder(context = LocalContext.current) + .data(url) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors2.surface.level3, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) +} + +private const val SCALE_FACTOR = 1.5f +private const val INNER_SHADOW_COLOR_START = 0x00000000 +private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF + +private const val BORDER_COLOR = 0xFFF0F0F0 +private const val OVERLAY_DARK = 0xFF141414 + +// region Previews + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OpportunitiesBGPreview() { + TangemThemePreview { + OpportunitiesBG( + modifier = Modifier.size(400.dp), + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + content = {}, + ) + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt index b1a24c2067..026dc41e70 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -16,8 +16,8 @@ import kotlin.math.max /** * A custom layout composable that arranges its children in a row with specific layout IDs. */ -internal enum class TangemRowLayoutId { - HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP +enum class TangemRowLayoutId { + HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM } /** @@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId { */ @Suppress("LongMethod") @Composable -internal fun TangemRowContainer( +fun TangemRowContainer( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3), content: @Composable () -> Unit, @@ -37,6 +37,7 @@ internal fun TangemRowContainer( val density = LocalDensity.current val localDirection = LocalLayoutDirection.current val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() } + val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() } val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() } val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() } val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() } @@ -45,7 +46,7 @@ internal fun TangemRowContainer( content = content, modifier = modifier, ) { measurables, constraints -> - val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding + val layoutWidth = max(0, constraints.maxWidth - contentStartPadding - contentEndPadding) val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() @@ -110,6 +111,10 @@ internal fun TangemRowContainer( layoutId = TangemRowLayoutId.EXTRA_TOP, constraints = constraints, ) + val extraBottomPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.EXTRA_BOTTOM, + constraints = constraints, + ) val mainLayoutHeight = maxOf( headPlaceable.heightOrZero(), @@ -124,7 +129,13 @@ internal fun TangemRowContainer( contentTopPadding } - val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding + val mainContentBottomPadding = if (extraBottomPlaceable != null) { + extraBottomPlaceable.heightOrZero() + contentBottomPadding + } else { + contentBottomPadding + } + + val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding layout(width = constraints.maxWidth, height = layoutHeight) { extraTopPlaceable?.placeRelative(x = 0, y = 0) @@ -174,6 +185,11 @@ internal fun TangemRowContainer( x = layoutWidth - tailPlaceable.width + contentEndPadding, y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2), ) + + extraBottomPlaceable?.placeRelative( + x = 0, + y = mainContentTopPadding + mainLayoutHeight + extraContentPadding, + ) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt index f5af27f6c1..64d73f459d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt @@ -1,22 +1,18 @@ 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 @@ -25,13 +21,13 @@ 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.ds.row.internal.TangemRowTail +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.TokenElementsTestTags +import org.burnoutcrew.reorderable.ReorderableLazyListState /** * UI model for header row component @@ -40,12 +36,19 @@ import com.tangem.core.ui.test.TokenElementsTestTags * @param modifier Modifier for the composable */ @Composable -fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) { +fun TangemHeaderRow( + headerRowUM: TangemHeaderRowUM, + modifier: Modifier = Modifier, + reorderableState: ReorderableLazyListState? = null, + isBalanceHidden: Boolean = false, +) { TangemHeaderRow( headTangemIconUM = headerRowUM.startIconUM, - footerTangemIconRes = headerRowUM.endIconRes, + tailUM = headerRowUM.tailUM, title = headerRowUM.title, subtitle = headerRowUM.subtitle, + isBalanceHidden = isBalanceHidden, + reorderableState = reorderableState, modifier = modifier, ) } @@ -56,16 +59,17 @@ fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifie * @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 tailUM Optional TailUM for the tail content * @param titleContent Composable lambda for the title content * @param headContent Composable lambda for the head content */ @Composable fun TangemHeaderRow( modifier: Modifier = Modifier, + isBalanceHidden: Boolean = false, subtitle: TextReference? = null, onItemClick: (() -> Unit)? = null, - @DrawableRes footerTangemIconRes: Int? = null, + tailUM: TangemRowTailUM = TangemRowTailUM.Empty, titleContent: @Composable (Modifier) -> Unit, headContent: @Composable (Modifier) -> Unit, ) { @@ -92,7 +96,7 @@ fun TangemHeaderRow( ) { val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } Text( - text = wrappedSubtitle.resolveAnnotatedReference(), + text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, maxLines = 1, @@ -102,17 +106,7 @@ fun TangemHeaderRow( ) } 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), - ) - } + TangemRowTail(tangemRowTailUM = tailUM) } } @@ -131,9 +125,11 @@ fun TangemHeaderRow( fun TangemHeaderRow( title: TextReference, modifier: Modifier = Modifier, + isBalanceHidden: Boolean = false, subtitle: TextReference? = null, headTangemIconUM: TangemIconUM? = null, - @DrawableRes footerTangemIconRes: Int? = null, + tailUM: TangemRowTailUM = TangemRowTailUM.Empty, + reorderableState: ReorderableLazyListState? = null, isEnabled: Boolean = false, onItemClick: (() -> Unit)? = null, ) { @@ -172,7 +168,7 @@ fun TangemHeaderRow( ) { val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } Text( - text = wrappedSubtitle.resolveAnnotatedReference(), + text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, maxLines = 1, @@ -182,17 +178,10 @@ fun TangemHeaderRow( ) } 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), - ) - } + TangemRowTail( + tangemRowTailUM = tailUM, + reorderableState = reorderableState, + ) } } @@ -217,7 +206,25 @@ private class PreviewProvider : PreviewParameterProvider { startIconUM = TangemIconUM.Currency( currencyIconState = CurrencyIconState.Locked, ), - endIconRes = R.drawable.ic_minimize_24, + tailUM = TangemRowTailUM.Empty, + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "1", + startIconUM = TangemIconUM.Currency( + currencyIconState = CurrencyIconState.Locked, + ), + tailUM = TangemRowTailUM.Icon(R.drawable.ic_arrow_collapse_24), + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "1", + startIconUM = TangemIconUM.Currency( + currencyIconState = CurrencyIconState.Locked, + ), + tailUM = TangemRowTailUM.Icon(R.drawable.ic_group_drop_24), title = stringReference("Account"), subtitle = stringReference("\$ 42,900.17"), ), 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 index fd69abb660..28c3d44501 100644 --- 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 @@ -1,9 +1,9 @@ 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.ds.row.internal.TangemRowTailUM import com.tangem.core.ui.extensions.TextReference /** @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference * @param title Title text reference * @param subtitle Subtitle text reference (optional) * @param startIconUM Icon UI model (optional) - * @param endIconRes Icon UI model (optional) + * @param tailUM Tail UI model (optional) * @param isEnabled Flag indicating if click is enabled * @param onItemClick Callback for item click (optional) */ @@ -23,7 +23,7 @@ data class TangemHeaderRowUM( val title: TextReference, val subtitle: TextReference? = null, val startIconUM: TangemIconUM? = null, - @DrawableRes val endIconRes: Int? = null, + val tailUM: TangemRowTailUM = TangemRowTailUM.Empty, 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/internal/TokenRowTail.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt similarity index 59% rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt index f3416f8a29..913d8b2bea 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt @@ -1,7 +1,9 @@ -package com.tangem.core.ui.ds.row.token.internal +package com.tangem.core.ui.ds.row.internal +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon @@ -9,11 +11,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow -import com.tangem.core.ui.R -import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.TangemTheme @@ -22,37 +23,48 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder @Composable -internal fun TokenRowTail( - tailUM: TangemTokenRowUM.TailUM, - reorderableTokenListState: ReorderableLazyListState?, +internal fun TangemRowTail( + tangemRowTailUM: TangemRowTailUM, modifier: Modifier = Modifier, + reorderableState: ReorderableLazyListState? = null, ) { AnimatedContent( - targetState = tailUM, + targetState = tangemRowTailUM, label = "Update non content fiat block", - modifier = modifier, + modifier = modifier.height(TangemTheme.dimens2.x4), contentKey = { it::class.java }, ) { animatedState -> val innerModifier = Modifier.padding(start = TangemTheme.dimens2.x2) when (animatedState) { - TangemTokenRowUM.TailUM.Empty -> Unit - is TangemTokenRowUM.TailUM.Draggable -> DraggableImage( - reorderableTokenListState = reorderableTokenListState, + TangemRowTailUM.Empty -> Unit + is TangemRowTailUM.Draggable -> DraggableImage( + iconRes = animatedState.iconRes, + reorderableState = reorderableState, modifier = innerModifier, ) - is TangemTokenRowUM.TailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier) + is TangemRowTailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier) + is TangemRowTailUM.Icon -> Icon( + imageVector = ImageVector.vectorResource(animatedState.iconRes), + contentDescription = null, + modifier = modifier, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + ) } } } @Composable -private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) { +private fun DraggableImage( + @DrawableRes iconRes: Int, + reorderableState: ReorderableLazyListState?, + modifier: Modifier = Modifier, +) { Box( modifier = modifier .size(size = TangemTheme.dimens2.x6) .then( - other = if (reorderableTokenListState != null) { - Modifier.detectReorder(reorderableTokenListState) + other = if (reorderableState != null) { + Modifier.detectReorder(reorderableState) } else { Modifier }, @@ -61,7 +73,7 @@ private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, contentAlignment = Alignment.Center, ) { Icon( - painter = painterResource(id = R.drawable.ic_drag_24), + imageVector = ImageVector.vectorResource(iconRes), tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, contentDescription = null, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt new file mode 100644 index 0000000000..841faea488 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt @@ -0,0 +1,25 @@ +package com.tangem.core.ui.ds.row.internal + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +/** + * Tail UI model for row components + */ +@Immutable +sealed class TangemRowTailUM { + data class Text( + val text: TextReference, + ) : TangemRowTailUM() + + data class Icon( + @DrawableRes val iconRes: Int, + ) : TangemRowTailUM() + + data class Draggable( + @DrawableRes val iconRes: Int, + ) : TangemRowTailUM() + + data object Empty : TangemRowTailUM() +} \ 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 a0b50fe199..cd2b93a853 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter 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.internal.TangemRowTail import com.tangem.core.ui.ds.row.token.internal.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -30,16 +31,16 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState * * [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 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. + * @param tokenRowUM The user model containing the data for the token row. + * @param isBalanceHidden A boolean indicating whether the balance should be hidden. + * @param reorderableState 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?, + reorderableState: ReorderableLazyListState?, modifier: Modifier = Modifier, ) { TangemRowContainer( @@ -52,15 +53,6 @@ fun TangemTokenRow( .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(), - ) - TokenRowTitle( titleUM = tokenRowUM.titleUM, modifier = Modifier @@ -77,29 +69,42 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), ) - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = tokenRowUM.topEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.bodySemibold16, + textColor = TangemTheme.colors2.text.neutral.primary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_TOP) .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), ) - TokenRowEndBottomContent( + TokenRowEndContent( endContentUM = tokenRowUM.bottomEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.captionSemibold12, + textColor = TangemTheme.colors2.text.neutral.secondary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), ) - TokenRowTail( - tailUM = tokenRowUM.tailUM, - reorderableTokenListState = reorderableTokenListState, + TangemRowTail( + tangemRowTailUM = tokenRowUM.tailUM, + reorderableState = reorderableState, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.TAIL) .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) + + TokenRowPromoBanner( + promoBannerUM = tokenRowUM.promoBannerUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2) + .fillMaxWidth(), + ) }, modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), ) @@ -110,18 +115,18 @@ fun TangemTokenRow( * * [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. + * @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 reorderableState 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?, + reorderableState: ReorderableLazyListState?, modifier: Modifier = Modifier, headComponent: @Composable (Modifier) -> Unit, titleComponent: @Composable (Modifier) -> Unit, @@ -159,25 +164,29 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), ) - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = tokenRowUM.topEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.bodySemibold16, + textColor = TangemTheme.colors2.text.neutral.primary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_TOP) .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), ) - TokenRowEndBottomContent( + TokenRowEndContent( endContentUM = tokenRowUM.bottomEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.captionSemibold12, + textColor = TangemTheme.colors2.text.neutral.secondary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), ) - TokenRowTail( - tailUM = tokenRowUM.tailUM, - reorderableTokenListState = reorderableTokenListState, + TangemRowTail( + tangemRowTailUM = tokenRowUM.tailUM, + reorderableState = reorderableState, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.TAIL) .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), @@ -218,19 +227,20 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TangemTokenRow_Preview( - @PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM, + @PreviewParameter(TangemTokenRow_PreviewProvider::class) tokenRowUM: TangemTokenRowUM, ) { TangemThemePreviewRedesign { TangemTokenRow( tokenRowUM = tokenRowUM, isBalanceHidden = false, - reorderableTokenListState = null, + reorderableState = null, modifier = Modifier.background(TangemTheme.colors2.surface.level1), ) } } -private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider( +@Suppress("ClassNaming") +class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider( collection = listOf( TangemTokenRowPreviewData.defaultState, TangemTokenRowPreviewData.defaultEllipsisState, 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 98027ff5dd..c183f9863b 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 @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.ds.badge.TangemBadgeUM import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -32,7 +33,7 @@ sealed class TangemTokenRowUM : TangemRowUM { abstract val bottomEndContentUM: EndContentUM /** Token row tail UM */ - abstract val tailUM: TailUM + abstract val tailUM: TangemRowTailUM /** Promo banner UM */ abstract val promoBannerUM: PromoBannerUM @@ -54,7 +55,7 @@ sealed class TangemTokenRowUM : TangemRowUM { override val topEndContentUM: EndContentUM, override val bottomEndContentUM: EndContentUM, override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty, - override val tailUM: TailUM = TailUM.Empty, + override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty, override val onItemClick: (() -> Unit)?, override val onItemLongClick: (() -> Unit)?, ) : TangemTokenRowUM() @@ -71,7 +72,7 @@ sealed class TangemTokenRowUM : TangemRowUM { override val topEndContentUM: EndContentUM = EndContentUM.Loading override val bottomEndContentUM: EndContentUM = EndContentUM.Loading override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty - override val tailUM: TailUM = TailUM.Empty + override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty override val onItemClick: (() -> Unit)? = null override val onItemLongClick: (() -> Unit)? = null } @@ -84,7 +85,7 @@ sealed class TangemTokenRowUM : TangemRowUM { override val headIconUM: TangemIconUM.Currency, override val titleUM: TitleUM, override val subtitleUM: SubtitleUM, - override val tailUM: TailUM, + override val tailUM: TangemRowTailUM, override val onItemClick: (() -> Unit)?, override val onItemLongClick: (() -> Unit)?, override val topEndContentUM: EndContentUM = EndContentUM.Empty, @@ -133,7 +134,8 @@ sealed class TangemTokenRowUM : TangemRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val startIcons: ImmutableList = persistentListOf(), + val endIcons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() @@ -153,15 +155,4 @@ sealed class TangemTokenRowUM : TangemRowUM { data object Empty : PromoBannerUM() } - - @Immutable - sealed class TailUM { - data class Text( - val text: TextReference, - ) : TailUM() - - data object Draggable : TailUM() - - data object Empty : TailUM() - } } \ 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 ce68a8771d..fb7b25744f 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 @@ -7,6 +7,7 @@ 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.internal.TangemRowTailUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference @@ -123,7 +124,7 @@ internal object TangemTokenRowPreviewData { topEndContentUM = topEndContentUM, bottomEndContentUM = bottomEndContentUM, promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, - tailUM = TangemTokenRowUM.TailUM.Empty, + tailUM = TangemRowTailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) @@ -142,7 +143,7 @@ internal object TangemTokenRowPreviewData { ) }), ), - icons = persistentListOf( + startIcons = persistentListOf( TangemIconUM.Icon(R.drawable.ic_staking_mini_10), TangemIconUM.Icon(R.drawable.ic_attention_12), TangemIconUM.Icon(R.drawable.ic_error_sync_24), @@ -150,7 +151,7 @@ internal object TangemTokenRowPreviewData { ), bottomEndContentUM = bottomEndContentUM, promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, - tailUM = TangemTokenRowUM.TailUM.Empty, + tailUM = TangemRowTailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) @@ -164,7 +165,7 @@ internal object TangemTokenRowPreviewData { topEndContentUM = topEndContentUM, bottomEndContentUM = bottomEndContentUM, promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, - tailUM = TangemTokenRowUM.TailUM.Empty, + tailUM = TangemRowTailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) @@ -178,7 +179,7 @@ internal object TangemTokenRowPreviewData { topEndContentUM = topEndContentUM, bottomEndContentUM = bottomEndContentUM, promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, - tailUM = TangemTokenRowUM.TailUM.Empty, + tailUM = TangemRowTailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) @@ -189,7 +190,7 @@ internal object TangemTokenRowPreviewData { headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, - tailUM = TangemTokenRowUM.TailUM.Draggable, + tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24), onItemClick = {}, onItemLongClick = {}, ) @@ -202,7 +203,7 @@ internal object TangemTokenRowPreviewData { subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, bottomEndContentUM = bottomEndContentUM, - tailUM = TangemTokenRowUM.TailUM.Draggable, + tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24), onItemClick = {}, onItemLongClick = {}, ) 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 deleted file mode 100644 index 376e670bda..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.core.ui.ds.row.token.internal - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.width -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.style.TextOverflow -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.components.TextShimmer -import com.tangem.core.ui.components.marketprice.PriceChangeState -import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.ds.row.token.TangemTokenRowUM -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreviewRedesign - -@Composable -internal fun TokenRowEndBottomContent( - endContentUM: TangemTokenRowUM.EndContentUM, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - when (endContentUM) { - is TangemTokenRowUM.EndContentUM.Content -> Content( - modifier = modifier, - endContentUM = endContentUM, - isBalanceHidden = isBalanceHidden, - ) - TangemTokenRowUM.EndContentUM.Empty -> Unit - TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.captionSemibold12, - modifier = modifier.width(TangemTheme.dimens2.x10), - radius = TangemTheme.dimens2.x25, - ) - } -} - -@Composable -private fun Content( - endContentUM: TangemTokenRowUM.EndContentUM.Content, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( - isEnabled = endContentUM.isFlickering, - textColor = if (endContentUM.isAvailable) { - TangemTheme.colors2.text.neutral.secondary - } else { - TangemTheme.colors2.text.status.disabled - }, - ), - ) - - when (val priceChangeUM = endContentUM.priceChangeUM) { - is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = priceChangeUM, - isFlickering = endContentUM.isFlickering, - isAvailable = endContentUM.isAvailable, - ) - PriceChangeState.Unknown -> Unit - } - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndBottomContent_Preview( - @PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, -) { - TangemThemePreviewRedesign { - TokenRowEndBottomContent( - endContentUM = params, - isBalanceHidden = false, - ) - } -} - -private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - TangemTokenRowPreviewData.bottomEndContentUM, - ) -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt similarity index 64% rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt index 131497424f..934b33aa8c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt @@ -8,15 +8,18 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow 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.util.fastForEach import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.orMaskWithStars @@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -internal fun TokenRowEndTopContent( +internal fun TokenRowEndContent( endContentUM: TangemTokenRowUM.EndContentUM, isBalanceHidden: Boolean, + textStyle: TextStyle, + textColor: Color, modifier: Modifier = Modifier, ) { when (endContentUM) { @@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent( modifier = modifier, endContentUM = endContentUM, isBalanceHidden = isBalanceHidden, + textStyle = textStyle, + textColor = textColor, ) TangemTokenRowUM.EndContentUM.Empty -> Unit TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.bodySemibold16, - modifier = modifier.width(TangemTheme.dimens2.x18), + style = textStyle, + modifier = modifier.width(TangemTheme.dimens2.x10), radius = TangemTheme.dimens2.x25, ) } @@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent( @Composable private fun Content( endContentUM: TangemTokenRowUM.EndContentUM.Content, + textStyle: TextStyle, + textColor: Color, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { @@ -56,14 +65,14 @@ private fun Content( verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( - visible = endContentUM.icons.isNotEmpty(), + visible = endContentUM.startIcons.isNotEmpty(), ) { Row( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { - endContentUM.icons.fastForEach { icon -> + endContentUM.startIcons.fastForEach { icon -> Icon( modifier = Modifier.size(TangemTheme.dimens2.x3), painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), @@ -75,11 +84,11 @@ private fun Content( } Text( - modifier = Modifier, text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + color = textColor, + style = textStyle.applyBladeBrush( isEnabled = endContentUM.isFlickering, textColor = if (endContentUM.isAvailable) { TangemTheme.colors2.text.neutral.primary @@ -88,6 +97,34 @@ private fun Content( }, ), ) + + AnimatedVisibility( + visible = endContentUM.endIcons.isNotEmpty(), + ) { + Row( + modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + endContentUM.endIcons.fastForEach { icon -> + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x3), + painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), + tint = icon.tintReference(), + contentDescription = null, + ) + } + } + } + + when (val priceChangeUM = endContentUM.priceChangeUM) { + is PriceChangeState.Content -> TokenRowPriceChangeContent( + priceChangeState = priceChangeUM, + isFlickering = endContentUM.isFlickering, + isAvailable = endContentUM.isAvailable, + ) + PriceChangeState.Unknown -> Unit + } } } @@ -95,13 +132,15 @@ private fun Content( @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndTopContent_Preview( +private fun TokenRowEndContent_Preview( @PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, ) { TangemThemePreviewRedesign { - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = params, isBalanceHidden = false, + textColor = TangemTheme.colors2.text.neutral.primary, + textStyle = TangemTheme.typography2.captionSemibold12, ) } } @@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview( private class TokenRowEndContentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - TangemTokenRowPreviewData.topEndContentUM, + TangemTokenRowPreviewData.bottomEndContentUM, ) } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt index ef635a0a8e..052c994ac7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt @@ -24,6 +24,12 @@ internal fun RowScope.TokenRowPriceChangeContent( isFlickering: Boolean, isAvailable: Boolean = true, ) { + val color = when (priceChangeState.type) { + PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent + PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.tertiary + } + AnimatedContent( targetState = priceChangeState.type, label = "Update the price change's arrow", @@ -39,11 +45,7 @@ internal fun RowScope.TokenRowPriceChangeContent( }, ), ), - tint = when (animatedType) { - PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent - PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary - }, + tint = color, contentDescription = null, modifier = Modifier.size(TangemTheme.dimens2.x3), ) @@ -61,7 +63,7 @@ internal fun RowScope.TokenRowPriceChangeContent( style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( isEnabled = isFlickering, textColor = if (isAvailable) { - TangemTheme.colors2.text.neutral.secondary + color } else { TangemTheme.colors2.text.status.disabled }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt index 93845dea18..013204a20f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.ripple 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.vector.ImageVector @@ -19,6 +16,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -40,55 +39,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C LaunchedEffect(promoBannerUM) { promoBannerUM.onPromoShown() } - val bgColor = TangemTheme.colors.control.default - Column(modifier = modifier) { + val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen + Column( + modifier = modifier, + ) { + Icon( + painter = painterResource(id = R.drawable.shape_triangular), + contentDescription = null, + tint = bgColor, + modifier = Modifier.padding(start = TangemTheme.dimens2.x5), + ) Row( modifier = Modifier .background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4)) .clickable(onClick = promoBannerUM.onPromoBannerClick) - .padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2) - .fillMaxWidth(), + .padding( + start = TangemTheme.dimens2.x2_5, + end = TangemTheme.dimens2.x0_5, + top = TangemTheme.dimens2.x0_5, + bottom = TangemTheme.dimens2.x0_5, + ), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), contentDescription = null, - tint = TangemTheme.colors.icon.accent, + tint = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .padding(end = TangemTheme.dimens2.x2) - .size(TangemTheme.dimens2.x4), + .padding(vertical = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x3), ) Text( text = promoBannerUM.title.resolveReference(), - style = TangemTheme.typography2.captionSemibold12, - color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionSemibold11, + color = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .weight(1f) - .padding(end = TangemTheme.dimens2.x2), + .padding(vertical = TangemTheme.dimens2.x0_5), ) - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors2.text.neutral.secondary, - modifier = Modifier - .size(TangemTheme.dimens2.x4) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = { promoBannerUM.onCloseClick() }, - ), - ) - } - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_rectangle_bottom), - contentDescription = null, - tint = bgColor, - modifier = Modifier - .size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2), + TangemBadge( + size = TangemBadgeSize.X4, + shape = TangemBadgeShape.Rounded, + color = TangemBadgeColor.Green, + type = TangemBadgeType.Tinted, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_close_24), + iconPosition = TangemBadgeIconPosition.None, + onClick = promoBannerUM.onCloseClick, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt index 67e1989fae..54d39c5104 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt @@ -45,7 +45,7 @@ fun TangemTab( val backgroundColor = if (isChecked) { TangemTheme.colors2.tabs.backgroundPrimary } else { - TangemTheme.colors2.tabs.textPrimary + TangemTheme.colors2.tabs.backgroundSecondary } val textColor = if (isChecked) { TangemTheme.colors2.tabs.textPrimary diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index b96dd8d788..c9dca9b4fc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -2,22 +2,26 @@ package com.tangem.core.ui.ds.topbar import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle 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 +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -30,11 +34,9 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign * @param title The title text to be displayed in the center of the top bar. * @param modifier Modifier to be applied to the top bar. * @param subtitle Optional subtitle text to be displayed below the title. - * @param startIconRes Optional drawable resource ID for the start icon. - * @param onStartContentClick Optional click action for the start icon. - * @param endIconRes Optional drawable resource ID for the end icon. - * @param onEndContentClick Optional click action for the end icon. - * @param isGhostButtons Flag to determine if ghost button styling should be applied. + * @param startActionUM Optional action data for the start action icon. + * @param endActionUM Optional action data for the end action icon. + * @param titleIconRes Optional drawable resource ID for the icon to be displayed next to the title. * [REDACTED_AUTHOR] */ @@ -43,63 +45,109 @@ fun TangemTopBar( modifier: Modifier = Modifier, title: TextReference? = null, subtitle: TextReference? = null, - @DrawableRes startIconRes: Int? = null, - onStartContentClick: (() -> Unit)? = null, - @DrawableRes endIconRes: Int? = null, - onEndContentClick: (() -> Unit)? = null, + startActionUM: TangemTopBarActionUM? = null, + endActionUM: TangemTopBarActionUM? = null, @DrawableRes titleIconRes: Int? = null, - titleStyle: TextStyle = TangemTheme.typography2.headingSemibold17, - isGhostButtons: Boolean = false, ) { - TangemTopBarInner( + TangemTopBar( + title = title, + subtitle = subtitle, + titleIconRes = titleIconRes, modifier = modifier, - content = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), - ) { - TangemTopBarTitle(title = title, titleIconRes = titleIconRes, titleStyle = titleStyle) - AnimatedVisibility( - visible = subtitle != null, - label = "Subtitle Visibility", - ) { - val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } - Text( - text = wrappedSubtitle.resolveAnnotatedReference(), - color = TangemTheme.colors2.text.neutral.secondary, - style = TangemTheme.typography2.bodyRegular15, - textAlign = TextAlign.Center, - maxLines = 1, - ) - } - } - }, - startContent = if (startIconRes != null) { - { TangemTopBarIcon(iconRes = startIconRes) } + startContent = if (startActionUM != null) { + { TangemTopBarActionContent(startActionUM) } } else { null }, - onStartContentClick = onStartContentClick, - endContent = if (endIconRes != null) { - { TangemTopBarIcon(iconRes = endIconRes) } + endContent = if (endActionUM != null) { + { TangemTopBarActionContent(endActionUM) } } else { null }, - onEndContentClick = onEndContentClick, - isGhostButtons = isGhostButtons, ) } +/** + * A top bar composable that displays a title and optional start and end icons. + * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev) + * + * @param title The title text to be displayed in the center of the top bar. + * @param modifier Modifier to be applied to the top bar. + * @param subtitle Optional subtitle text to be displayed below the title. + * +[REDACTED_AUTHOR] + */ @Composable -private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?, titleStyle: TextStyle) { +fun TangemTopBar( + modifier: Modifier = Modifier, + title: TextReference? = null, + subtitle: TextReference? = null, + @DrawableRes titleIconRes: Int? = null, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens2.x16) + .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), + ) { + AnimatedVisibility( + visible = startContent != null, + modifier = Modifier.align(Alignment.CenterStart), + label = "Start Content Visibility", + ) { + startContent?.invoke() + } + + Column( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), + ) { + TangemTopBarTitle(title = title, titleIconRes = titleIconRes) + AnimatedVisibility( + visible = subtitle != null, + label = "Subtitle Visibility", + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.bodyRegular15, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } + } + AnimatedVisibility( + visible = endContent != null, + modifier = Modifier.align(Alignment.CenterEnd), + label = "End Content Visibility", + ) { + endContent?.invoke() + } + } +} + +@Composable +private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?) { AnimatedVisibility( visible = title != null, label = "Title Visibility", + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), ) { val wrappedTitle = remember(this) { requireNotNull(title) } Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens2.x1, + alignment = Alignment.CenterHorizontally, + ), verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( @@ -120,7 +168,7 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Text( text = wrappedTitle.resolveAnnotatedReference(), color = TangemTheme.colors2.text.neutral.primary, - style = titleStyle, + style = TangemTheme.typography2.headingSemibold17, textAlign = TextAlign.Center, maxLines = 1, ) @@ -129,12 +177,29 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: } @Composable -private fun TangemTopBarIcon(@DrawableRes iconRes: Int) { +fun TangemTopBarActionContent( + actionUM: TangemTopBarActionUM, + modifier: Modifier = Modifier, + iconSize: Dp = TangemTheme.dimens2.x8, +) { + val background = lerp( + start = Color.Transparent, + stop = TangemTheme.colors2.button.backgroundSecondary, + fraction = actionUM.ghostModeProgress, + ) + val padding = (TangemTheme.dimens2.x10 - iconSize) / 2 Icon( - imageVector = ImageVector.vectorResource(id = iconRes), + imageVector = ImageVector.vectorResource(id = actionUM.iconRes), contentDescription = null, tint = TangemTheme.colors2.graphic.neutral.primary, - modifier = Modifier.fillMaxSize(), + modifier = modifier + .size(TangemTheme.dimens2.x10) + .clip(CircleShape) + .conditional(actionUM.isActionable) { background(background) } + .conditionalCompose(actionUM.isActionable && actionUM.onClick != null) { + clickableSingle(onClick = requireNotNull(actionUM.onClick)) + } + .padding(padding), ) } @@ -147,13 +212,10 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param TangemTopBar( title = params.title, subtitle = params.subtitle, - startIconRes = params.startIconRes, - endIconRes = params.endIconRes, titleIconRes = params.titleIconRes, - isGhostButtons = params.isGhostButtons, - onStartContentClick = {}, - onEndContentClick = {}, modifier = Modifier.background(TangemTheme.colors2.surface.level1), + startContent = params.startActionUM?.let { { TangemTopBarActionContent(it) } }, + endContent = params.endActionUM?.let { { TangemTopBarActionContent(it) } }, ) } } @@ -161,10 +223,9 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param private class TangemTopBarPreviewData( val title: TextReference? = null, val subtitle: TextReference? = null, - val isGhostButtons: Boolean = false, val titleIconRes: Int? = null, - val startIconRes: Int? = null, - val endIconRes: Int? = null, + val startActionUM: TangemTopBarActionUM? = null, + val endActionUM: TangemTopBarActionUM? = null, ) private class PreviewProvider : PreviewParameterProvider { @@ -172,41 +233,79 @@ private class PreviewProvider : PreviewParameterProvider Unit)? = null, + @param:FloatRange(0.0, 1.0) val ghostModeProgress: Float = 0f, +) \ No newline at end of file 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 deleted file mode 100644 index e445ea4a30..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.tangem.core.ui.ds.topbar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -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 -import com.tangem.core.ui.res.TangemTheme - -/** - * Internal top bar composable that arranges optional start, center, and end content. - * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev) - * - * @param modifier Modifier to be applied to the top bar. - * @param content Center content of the top bar. - * @param startContent Optional start content of the top bar. - * @param onStartContentClick Optional click action for the start content. - * @param endContent Optional end content of the top bar. - * @param onEndContentClick Optional click action for the end content. - * @param isGhostButtons Flag to determine if ghost button styling should be applied. - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun TangemTopBarInner( - modifier: Modifier = Modifier, - content: (@Composable () -> Unit)? = null, - startContent: (@Composable () -> Unit)? = null, - onStartContentClick: (() -> Unit)? = null, - endContent: (@Composable () -> Unit)? = null, - onEndContentClick: (() -> Unit)? = null, - isGhostButtons: Boolean = false, -) { - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() } - Box( - modifier = modifier - .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)) - .conditionalCompose(isGhostButtons) { - background(TangemTheme.colors2.button.backgroundSecondary) - } - - AnimatedVisibility( - visible = startContent != null, - modifier = Modifier.align(Alignment.CenterStart), - label = "Start Content Visibility", - ) { - Box( - modifier = iconModifier - .conditional(onStartContentClick != null) { - clickableSingle { onStartContentClick?.invoke() } - } - .conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) }, - ) { - startContent?.invoke() - } - } - - AnimatedVisibility( - visible = content != null, - modifier = Modifier.align(Alignment.Center), - ) { - content?.invoke() - } - - AnimatedVisibility( - visible = endContent != null, - modifier = Modifier.align(Alignment.CenterEnd), - label = "End Content Visibility", - ) { - Box( - modifier = iconModifier - .conditional(onEndContentClick != null) { - clickableSingle { onEndContentClick?.invoke() } - } - .conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) }, - ) { - endContent?.invoke() - } - } - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt new file mode 100644 index 0000000000..1d91d223b5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt @@ -0,0 +1,126 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import android.content.res.Configuration +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlin.math.max +import kotlin.math.roundToInt + + +@Composable +fun TangemCollapsingTopBar( + state: TangemCollapsingAppBarState, + collapsingPart: @Composable () -> Unit, + body: @Composable () -> Unit, +) { + Layout( + modifier = Modifier.fillMaxSize(), + content = { + collapsingPart() + body() + }, + ) { measurables, constraints -> + + val collapsingConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + ) + val collapsingPlaceable = measurables[0].measure(collapsingConstraints) + + val bodyConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + maxHeight = (constraints.maxHeight - collapsingConstraints.minHeight).coerceAtLeast(0), + ) + val bodyPlaceable = measurables[1].measure(bodyConstraints) + + val minHeight = 0.dp.roundToPx() + val maxHeight = collapsingPlaceable.height + minHeight + + val offset = state.heightOffset.roundToInt().coerceAtLeast(-maxHeight) + + val width = max( + collapsingPlaceable.width, + bodyPlaceable.width, + ).coerceIn(constraints.minWidth, constraints.maxWidth) + val height = max( + collapsingPlaceable.height, + bodyPlaceable.height, + ).coerceIn(constraints.minHeight, constraints.maxHeight) + + layout(width = width, height = height) { + bodyPlaceable.placeRelative(0, collapsingPlaceable.height + offset) + collapsingPlaceable.placeRelative(0, offset) + } + } +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * + * @property state The state of the collapsing app bar. + * @property snapAnimationSpec The animation spec used for snapping the app bar to its collapsed or + * expanded state after a fling. If null, no snapping will occur. + * @property flingAnimationSpec The decay animation spec used for fling gestures. + * If null, fling gestures will not be handled. + * @property nestedScrollConnection Nested scroll connection + */ +@Stable +data class TangemCollapsingAppBarBehavior( + val state: TangemCollapsingAppBarState, + val snapAnimationSpec: AnimationSpec?, + val flingAnimationSpec: DecayAnimationSpec?, + val nestedScrollConnection: NestedScrollConnection, +) + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemCollapsingTopBar_Preview() { + TangemThemePreviewRedesign { + val collapsingHeight = 200.dp + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = collapsingHeight, + ) + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(collapsingHeight) + .background(Color.Red), + ) + }, + body = { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .nestedScroll(behavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()), + ) + }, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt new file mode 100644 index 0000000000..c3e18a6df6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt @@ -0,0 +1,211 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import androidx.compose.animation.core.* +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection +import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState +import com.tangem.core.ui.utils.toPx +import kotlin.math.abs +import kotlin.math.absoluteValue + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param expandedHeight The height of the app bar when it is fully expanded. + * @param partialCollapsedHeight The height of the app bar when it is partially collapsed. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +fun rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight: Dp = -Int.MAX_VALUE.dp, + partialCollapsedHeight: Dp = expandedHeight, + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val topBarState = rememberTangemCollapsingAppBarState( + heightOffsetLimit = -expandedHeight.toPx(), + partialHeightLimit = partialCollapsedHeight.toPx(), + ) + return exitUntilCollapsedScrollBehavior( + state = topBarState, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + ) +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param state The state of the collapsing app bar, which controls the height offset and scroll behavior. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +private fun exitUntilCollapsedScrollBehavior( + state: TangemCollapsingAppBarState = rememberTangemCollapsingAppBarState(), + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val nestedScrollConnection = remember(state) { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy < 0) { + state.direction = TopBapScrollDirection.Collapsing + state.dispatchRawDelta(dy) + } else { + 0f + } + + return Offset(0f, consume) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy > 0) { + state.direction = TopBapScrollDirection.Expanding + state.dispatchRawDelta(dy) + } else { + state.direction = TopBapScrollDirection.Collapsing + 0f + } + + return Offset(0f, consume) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val superConsumed = super.onPostFling(consumed, available) + return superConsumed + settleAppBar( + state = state, + velocity = available.y, + flingAnimationSpec = flingAnimationSpec, + snapAnimationSpec = snapAnimationSpec, + ) + } + } + } + + return remember(state, nestedScrollConnection, snapAnimationSpec, flingAnimationSpec) { + TangemCollapsingAppBarBehavior( + state = state, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + nestedScrollConnection = nestedScrollConnection, + ) + } +} + +@Composable +fun Modifier.snapToExitUntilCollapsed(behavior: TangemCollapsingAppBarBehavior): Modifier { + return draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + behavior.state.heightOffset += delta + }, + onDragStopped = { velocity -> + settleAppBar( + state = behavior.state, + velocity = velocity, + flingAnimationSpec = behavior.flingAnimationSpec, + snapAnimationSpec = behavior.snapAnimationSpec, + ) + }, + ) +} + +/** + * Settles the app bar to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + */ +@Suppress("MagicNumber", "CyclomaticComplexMethod") +private suspend fun settleAppBar( + state: TangemCollapsingAppBarState, + velocity: Float, + flingAnimationSpec: DecayAnimationSpec?, + snapAnimationSpec: AnimationSpec?, + snapCollapseThreshold: Float = 0.3f, + snapExpandThreshold: Float = 0.7f, +): Velocity { + val partialLimit = state.heightOffsetLimit + state.partialHeightLimit + var remainingVelocity = velocity + + // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, + // and just return Zero Velocity. + // Note that we don't check for 0f due to float precision with the collapsedFraction + // calculation. + if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { + return Velocity.Zero + } + + // Fling + if (flingAnimationSpec != null && velocity.absoluteValue > 1f) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity, + ).animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + + val availableDelta = partialLimit - initialHeightOffset + + state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) { + (initialHeightOffset + delta).coerceAtLeast(partialLimit) + } else { + initialHeightOffset + delta + } + + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(maxOf(delta, availableDelta) - consumed) > 0.5f) this.cancelAnimation() + } + } + // Snap + if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) { + AnimationState(initialValue = state.heightOffset).animateTo( + when (state.direction) { + TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) { + partialLimit + } else { + 0f + } + TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) { + 0f + } else { + partialLimit + } + TopBapScrollDirection.Idle -> 0f + }, + animationSpec = snapAnimationSpec, + ) { + state.heightOffset = value + } + } + return Velocity(0f, remainingVelocity) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt new file mode 100644 index 0000000000..a5d1a8892d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt @@ -0,0 +1,142 @@ +package com.tangem.core.ui.ds.topbar.collapsing.entity + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.animateTo +import androidx.compose.animation.core.tween +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState.Companion.Saver +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min + +/** + * State of the collapsing top app bar. + * It contains the current height offset, the limits for collapsing and expanding, and the scroll direction. + * + * @property initialHeightOffset The initial height offset of the app bar. Default is 0f. + * @property heightOffsetLimit The height offset limit for full collapse. + * @property partialHeightLimit The height offset limit for partial collapse. Default is the same as [heightOffsetLimit] + */ +@Stable +class TangemCollapsingAppBarState( + val initialHeightOffset: Float = 0f, + val heightOffsetLimit: Float = 0f, + val partialHeightLimit: Float = heightOffsetLimit, +) : ScrollableState { + + private val _heightOffset = mutableFloatStateOf(initialHeightOffset) + private var deferredConsumption: Float = 0f + + /** + * The current height offset of the app bar. + * This value is updated as the user scrolls, and is constrained between [heightOffsetLimit] and 0f. + */ + var heightOffset: Float + get() = _heightOffset.floatValue + set(newOffset) { + _heightOffset.floatValue = + newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f) + } + + /** + * The fraction of the app bar that is collapsed, calculated as the ratio of [heightOffset] to [heightOffsetLimit]. + */ + val collapsedFraction: Float + get() = + if (heightOffsetLimit != 0f) { + heightOffset / heightOffsetLimit + } else { + 0f + } + + /** + * The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle. + */ + var direction: TopBapScrollDirection = TopBapScrollDirection.Idle + + private val scrollableState = ScrollableState { value -> + val consume = if (value < 0) { + max(heightOffsetLimit - heightOffset, value) + } else { + min(0f - heightOffset, value) + } + + val current = consume + deferredConsumption + val currentInt = current.toInt() + + if (current.absoluteValue > 0) { + heightOffset += currentInt + deferredConsumption = current - currentInt + } + + consume + } + + override val isScrollInProgress: Boolean + get() = scrollableState.isScrollInProgress + + /** + * + */ + suspend fun collapse() { + AnimationState(initialValue = heightOffset).animateTo( + targetValue = heightOffsetLimit + partialHeightLimit, + animationSpec = tween(), + ) { + heightOffset = value + } + } + + override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) = + scrollableState.scroll(scrollPriority, block) + + override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta) + + companion object { + /** The default [Saver] implementation for [TangemCollapsingAppBarState]. */ + val Saver: Saver = + listSaver( + save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) }, + restore = { state -> + TangemCollapsingAppBarState( + heightOffsetLimit = state[0], + partialHeightLimit = state[2], + initialHeightOffset = state[1], + ) + }, + ) + } +} + +/** + * Remembers and saves the state of the collapsing top app bar across recompositions and configuration changes. + */ +@Composable +fun rememberTangemCollapsingAppBarState( + heightOffsetLimit: Float = -Float.MAX_VALUE, + partialHeightLimit: Float = -Float.MAX_VALUE, + initialHeightOffset: Float = 0f, +): TangemCollapsingAppBarState { + return rememberSaveable(saver = Saver) { + TangemCollapsingAppBarState( + initialHeightOffset = initialHeightOffset, + partialHeightLimit = partialHeightLimit, + heightOffsetLimit = heightOffsetLimit, + ) + } +} + +/** + * The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle. + */ +enum class TopBapScrollDirection { + Collapsing, Expanding, Idle +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index cae9546835..89aaa014aa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE @@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } +open class BigDecimalCryptoFormatStyled( + val symbol: String, + val decimals: Int, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), + val shouldIgnoreSymbolPosition: Boolean = false, +) : BigDecimalFormatStyled { + + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} + // == Initializers == fun BigDecimalFormatScope.crypto( @@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto( ) } +fun BigDecimalFormatScope.cryptoStyled( + symbol: String, + decimals: Int, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = symbol, + decimals = decimals, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} + +fun BigDecimalFormatScope.cryptoStyled( + cryptoCurrency: CryptoCurrency, + spanStyleReference: SpanStyleReference, + ignoreSymbolPosition: Boolean = false, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + spanStyleReference = spanStyleReference, + shouldIgnoreSymbolPosition = ignoreSymbolPosition, + locale = locale, + ) +} + // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> @@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> } } +fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = + BigDecimalFormatStyled { value -> + if (shouldIgnoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + stringReference(NON_BREAKING_SPACE + symbol), + ) + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + ) + } + } + fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> val formatter = if (value.isMoreThanThreshold()) { NumberFormat.getCurrencyInstance(locale).apply { diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 5c41281344..a1f6549b2b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -1,9 +1,11 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -15,8 +17,16 @@ open class BigDecimalFiatFormat( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } -// == Initializers == +open class BigDecimalFiatFormatStyled( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormatStyled { + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} +//region == Initializers == fun BigDecimalFormatScope.fiat( fiatCurrencyCode: String, fiatCurrencySymbol: String, @@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat( ) } -// == Formatters == +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormatStyled { + return BigDecimalFiatFormatStyled( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} +// endregion == Formatters == /** * Formats fiat amount with default precision. @@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { } } +fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + val formattingAmount = if (value.isLessThanThreshold()) { + FIAT_FORMAT_THRESHOLD + } else { + value + } + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val formattedAmount = formatter.format(formattingAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + val wholePart = formattedAmount.take(separatorIndex) + val fractionalPart = formattedAmount.drop(separatorIndex) + + combinedReference( + if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY, + stringReference(wholePart), + styledStringReference(fractionalPart, spanStyleReference), + ) +} + /** * Formats fiat amount with default precision and adds tilde sign */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt index c685db7f1d..f7a87efe6a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -1,17 +1,27 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import java.math.BigDecimal interface BigDecimalFormatScope { - companion object { val Empty = object : BigDecimalFormatScope {} } + companion object { + val Empty = object : BigDecimalFormatScope {} + } } fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope +fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope + inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference { + return BigDecimalFormatScope.Empty.block()(this) +} + inline fun BigDecimal?.format( fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, block: BigDecimalFormatScope.() -> BigDecimalFormat, @@ -20,10 +30,26 @@ inline fun BigDecimal?.format( return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal?.formatStyled( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormatStyled, +): TextReference { + if (this == null) return stringReference(fallbackString) + return BigDecimalFormatScope.Empty.block()(this) +} + fun BigDecimal?.format( format: BigDecimalFormat, fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, ): String { if (this == null) return fallbackString return format(this) +} + +fun BigDecimal?.format( + format: BigDecimalFormatStyled, + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, +): TextReference { + if (this == null) return stringReference(fallbackString) + return format(this) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt index 5a34d92702..5e5ec69b89 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt @@ -10,11 +10,7 @@ internal class DefaultHapticManager( override fun perform(effect: TangemHapticEffect) { when (effect) { - is TangemHapticEffect.View -> { - effect.androidHapticFeedbackCode?.let { - ViewCompat.performHapticFeedback(view, it) - } - } + is TangemHapticEffect.View -> performViewEffect(effect) is TangemHapticEffect.OneTime -> { if (vibratorHapticManager != null) { vibratorHapticManager.performOneTime(effect) @@ -29,4 +25,20 @@ internal class DefaultHapticManager( } } } + + private fun performViewEffect(effect: TangemHapticEffect.View) { + val code = effect.androidHapticFeedbackCode ?: return + if (ViewCompat.performHapticFeedback(view, code)) return + + val fallbackCode = VIEW_FALLBACKS[effect]?.androidHapticFeedbackCode ?: return + ViewCompat.performHapticFeedback(view, fallbackCode) + } + + private companion object { + val VIEW_FALLBACKS = mapOf( + TangemHapticEffect.View.ClockTick to TangemHapticEffect.View.KeyboardPress, + TangemHapticEffect.View.Confirm to TangemHapticEffect.View.ContextClick, + TangemHapticEffect.View.Reject to TangemHapticEffect.View.LongPress, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 04801d50d7..9b81ff94d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -16,9 +16,11 @@ object TangemColorPalette { val Dark4 = Color(0xFF3B3B3B) val Dark5 = Color(0xFF303030) val Dark6 = Color(0xFF1E1E1E) + val Dark7 = Color(0xFF171717) // endregion Dark // region Dark Alpha + val Dark_05 = Color(0x0D1E1E1E) val Dark_10 = Color(0x1A1E1E1E) val Dark_20 = Color(0x331E1E1E) val Dark_30 = Color(0x4D1E1E1E) @@ -58,20 +60,45 @@ object TangemColorPalette { val DarkGreen = Color(0xFF06311F) // endregion Green - // region Blue + // region Azure val Azure = Color(0xFF0099FF) - // endregion Blue + val Azure_50 = Color(0x800099FF) + val Azure_10 = Color(0x1A0099FF) + // endregion Azure - // region Red + // region Amaranth val Amaranth = Color(0xFFFF3333) + val Amaranth_50 = Color(0x80FF3333) + val Amaranth_20 = Color(0x33FF3333) + val Amaranth_10 = Color(0x1AFF3333) + // endregion Amaranth + + // region Flamingo val Flamingo = Color(0xFFFF5B5B) - // endregion Red + val Flamingo_50 = Color(0x80FF5B5B) + val Flamingo_20 = Color(0x33FF5B5B) + val Flamingo_10 = Color(0x1AFF5B5B) + // endregion Flamingo // region Yellow val Tangerine = Color(0xFFFFB71B) val Mustard = Color(0xFFFDDE55) // endregion Yellow + // region Emerald + val Emerald = Color(0xFF34DF12) + val Emerald_50 = Color(0x8034DF12) + val Emerald_20 = Color(0x3334DF12) + val Emerald_10 = Color(0x1A34DF12) + // endregion Emerald + + // region Eucalyptus + val Eucalyptus = Color(0xFF0C9F3D) + val Eucalyptus_50 = Color(0x800C9F3D) + val Eucalyptus_20 = Color(0x330C9F3D) + val Eucalyptus_10 = Color(0x1A0C9F3D) + // endregion Eucalyptus + // region Overlay val Overlay1 = Color(0x66000000) val Overlay2 = Color(0xB2000000) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index c3d2a23e2a..61f297696c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -22,6 +22,7 @@ class TangemColors2 internal constructor( val skeleton: Skeleton, val markers: Markers, val tabs: Tabs, + val contextMenu: ContextMenu, ) { @Stable @@ -326,21 +327,32 @@ class TangemColors2 internal constructor( class Border internal constructor( val neutral: Neutral, val status: Status, + walletIcon: Color, ) { + var walletIcon by mutableStateOf(walletIcon) + private set @Stable class Neutral internal constructor( primary: Color, secondary: Color, + tertiary: Color, + quaternary: Color, ) { var primary by mutableStateOf(primary) private set var secondary by mutableStateOf(secondary) private set + var tertiary by mutableStateOf(tertiary) + private set + var quaternary by mutableStateOf(quaternary) + private set fun update(other: Neutral) { primary = other.primary secondary = other.secondary + tertiary = other.tertiary + quaternary = other.quaternary } } @@ -367,6 +379,7 @@ class TangemColors2 internal constructor( fun update(other: Border) { neutral.update(other.neutral) status.update(other.status) + walletIcon = other.walletIcon } } @@ -448,24 +461,36 @@ class TangemColors2 internal constructor( @Stable class Markers internal constructor( - backgroundSolidGray: Color, - backgroundDisabled: Color, - backgroundSolidBlue: Color, - textGray: Color, textDisabled: Color, - iconGray: Color, iconDisabled: Color, + backgroundDisabled: Color, + textGray: Color, + iconGray: Color, borderGray: Color, - backgroundTintedBlue: Color, + backgroundSolidGray: Color, + backgroundTintedGray: Color, textBlue: Color, + iconBlue: Color, + borderTintedBlue: Color, + backgroundSolidBlue: Color, + backgroundTintedBlue: Color, + textRed: Color, + iconRed: Color, + borderTintedRed: Color, backgroundSolidRed: Color, backgroundTintedRed: Color, - iconBlue: Color, - iconRed: Color, - textRed: Color, - backgroundTintedGray: Color, - borderTintedBlue: Color, - borderTintedRed: Color, + textGreen: Color, + iconGreen: Color, + borderTintedGreen: Color, + borderSolidColor: Color, + backgroundTintedGreen: Color, + backgroundSolidGreen: Color, + textGreenAlt: Color, + iconGreenAlt: Color, + borderTintedGreenAlt: Color, + borderSolidColorAlt: Color, + backgroundTintedGreenAlt: Color, + backgroundSolidGreenAlt: Color, ) { var backgroundSolidGray by mutableStateOf(backgroundSolidGray) private set @@ -504,6 +529,32 @@ class TangemColors2 internal constructor( var borderTintedRed by mutableStateOf(borderTintedRed) private set + var textGreen by mutableStateOf(textGreen) + private set + var iconGreen by mutableStateOf(iconGreen) + private set + var borderTintedGreen by mutableStateOf(borderTintedGreen) + private set + var borderSolidColor by mutableStateOf(borderSolidColor) + private set + var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen) + private set + var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen) + private set + var textGreenAlt by mutableStateOf(textGreenAlt) + private set + var iconGreenAlt by mutableStateOf(iconGreenAlt) + private set + var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt) + private set + var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt) + private set + + var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt) + private set + var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt) + private set + fun update(other: Markers) { backgroundSolidGray = other.backgroundSolidGray backgroundDisabled = other.backgroundDisabled @@ -523,6 +574,18 @@ class TangemColors2 internal constructor( backgroundTintedGray = other.backgroundTintedGray borderTintedBlue = other.borderTintedBlue borderTintedRed = other.borderTintedRed + textGreen = other.textGreen + iconGreen = other.iconGreen + borderTintedGreen = other.borderTintedGreen + borderSolidColor = other.borderSolidColor + backgroundTintedGreen = other.backgroundTintedGreen + backgroundSolidGreen = other.backgroundSolidGreen + textGreenAlt = other.textGreenAlt + iconGreenAlt = other.iconGreenAlt + borderTintedGreenAlt = other.borderTintedGreenAlt + borderSolidColorAlt = other.borderSolidColorAlt + backgroundTintedGreenAlt = other.backgroundTintedGreenAlt + backgroundSolidGreenAlt = other.backgroundSolidGreenAlt } } @@ -562,6 +625,18 @@ class TangemColors2 internal constructor( } } + @Stable + class ContextMenu internal constructor( + background: Color, + ) { + var background by mutableStateOf(background) + private set + + fun update(other: ContextMenu) { + background = other.background + } + } + fun update(other: TangemColors2) { text.update(other.text) graphic.update(other.graphic) @@ -575,5 +650,6 @@ class TangemColors2 internal constructor( skeleton.update(other.skeleton) markers.update(other.markers) tabs.update(other.tabs) + contextMenu.update(other.contextMenu) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index efa104abf2..ad14b94dbf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -79,12 +79,15 @@ private fun lightThemeColors2(): TangemColors2 { neutral = TangemColors2.Border.Neutral( primary = TangemColorPalette.Light3, secondary = TangemColorPalette.Light5, + tertiary = TangemColorPalette.Light_10, + quaternary = TangemColorPalette.Dark_10, ), status = TangemColors2.Border.Status( accent = TangemColorPalette.Azure, warning = TangemColorPalette.Amaranth, attention = TangemColorPalette.Tangerine, ), + walletIcon = TangemColorPalette.Dark_10, ) val overlay = TangemColors2.Overlay( overlayPrimary = TangemColorPalette.Overlay1, @@ -122,8 +125,8 @@ private fun lightThemeColors2(): TangemColors2 { val surface = TangemColors2.Surface( level1 = TangemColorPalette.White, level2 = TangemColorPalette.Light1V2, - level3 = TangemColorPalette.Light1V2, - level4 = TangemColorPalette.White, + level3 = TangemColorPalette.White, + level4 = TangemColorPalette.Light1V2, ) val controls = TangemColors2.Controls( backgroundChecked = TangemColorPalette.Dark6, @@ -154,16 +157,28 @@ private fun lightThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark1, iconDisabled = TangemColorPalette.Light2, borderGray = TangemColorPalette.Light3, - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, - backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + backgroundTintedRed = TangemColorPalette.Amaranth_10, iconBlue = TangemColorPalette.Azure, iconRed = TangemColorPalette.Amaranth, textRed = TangemColorPalette.Amaranth, backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Eucalyptus, + iconGreenAlt = TangemColorPalette.Eucalyptus, + borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + borderSolidColorAlt = TangemColorPalette.Eucalyptus_50, + backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Light2, @@ -174,6 +189,9 @@ private fun lightThemeColors2(): TangemColors2 { backgroundTertiary = TangemColorPalette.White, backgroundQuaternary = TangemColorPalette.Dark_20, ) + val contextMenu = TangemColors2.ContextMenu( + background = TangemColorPalette.Dark_05, + ) return TangemColors2( text = text, graphic = graphic, @@ -187,6 +205,7 @@ private fun lightThemeColors2(): TangemColors2 { skeleton = skeleton, markers = markers, tabs = tabs, + contextMenu = contextMenu, ) } @@ -229,12 +248,15 @@ private fun darkThemeColors2(): TangemColors2 { neutral = TangemColors2.Border.Neutral( primary = TangemColorPalette.Dark4, secondary = TangemColorPalette.Dark4, + tertiary = TangemColorPalette.Light_10, + quaternary = TangemColorPalette.Light_10, ), status = TangemColors2.Border.Status( accent = TangemColorPalette.Azure, warning = TangemColorPalette.Flamingo, attention = TangemColorPalette.Mustard, ), + walletIcon = TangemColorPalette.Light_10, ) val overlay = TangemColors2.Overlay( overlayPrimary = TangemColorPalette.Overlay1, @@ -270,8 +292,8 @@ private fun darkThemeColors2(): TangemColors2 { borderPrimary = TangemColorPalette.Light4, ) val surface = TangemColors2.Surface( - level1 = TangemColorPalette.Dark6, - level2 = TangemColorPalette.Black, + level1 = TangemColorPalette.Black, + level2 = TangemColorPalette.Dark7, level3 = TangemColorPalette.Dark6, level4 = TangemColorPalette.Dark5, ) @@ -304,7 +326,7 @@ private fun darkThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark2, iconDisabled = TangemColorPalette.Dark5, borderGray = TangemColorPalette.White.copy(alpha = 0.2f), - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), @@ -312,8 +334,20 @@ private fun darkThemeColors2(): TangemColors2 { iconRed = TangemColorPalette.Flamingo, textRed = TangemColorPalette.Flamingo, backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Emerald, + iconGreenAlt = TangemColorPalette.Emerald, + borderTintedGreenAlt = TangemColorPalette.Emerald_10, + borderSolidColorAlt = TangemColorPalette.Emerald_50, + backgroundTintedGreenAlt = TangemColorPalette.Emerald_10, + backgroundSolidGreenAlt = TangemColorPalette.Emerald, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Dark4, @@ -324,6 +358,9 @@ private fun darkThemeColors2(): TangemColors2 { backgroundTertiary = TangemColorPalette.Light_10, backgroundQuaternary = TangemColorPalette.Light_10, ) + val contextMenu = TangemColors2.ContextMenu( + background = TangemColorPalette.Light_10, + ) return TangemColors2( text = text, graphic = graphic, @@ -337,5 +374,6 @@ private fun darkThemeColors2(): TangemColors2 { skeleton = skeleton, markers = markers, tabs = tabs, + contextMenu = contextMenu, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index d1c1e10b32..cff7ef913b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemeRedesign /** * Interface representing a Compose screen with common theming and content composition properties. @@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView( uiDependencies = uiDependencies, overrideSystemBarColors = overrideSystemBarColors, ) { - ScreenContent(modifier = screenModifier) + TangemThemeRedesign { + ScreenContent(modifier = screenModifier) + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt new file mode 100644 index 0000000000..600ad9af39 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.shader + +class GlossyShader : TangemShader { + override val sksl: String = + """ +// The MIT License + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +uniform float uTime; +uniform vec3 uResolution; + +vec4 main( vec2 fragCoord ) +{ + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr; + + float d = -uTime * 0.5; + float a = 0.0; + for (float i = 0.0; i < 8.0; ++i) { + a += cos(i - d - a * uv.x); + d += sin(uv.y * i + a); + } + d += uTime * 0.5; + vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5); + col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5); + return vec4(col,1.0); +} + """ +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt new file mode 100644 index 0000000000..05ade74d32 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt @@ -0,0 +1,193 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.shader + +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +/** + * A shader that creates a colorful, flowing "northern lights" effect. + * @param colors The colors to display. The last provided color acts like a "background" + * @param speed Adjust the speed of the movement + * @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs + * +[REDACTED_AUTHOR] + */ +class NorthernLightsMeshGradientShader( + colors: Array, + speed: Float = 1f, + scale: Float = 2f, +) : TangemShader { + + private val colorCount = colors.size + private val colorUniforms = colors.flatMap { + listOf(it.red, it.green, it.blue) + }.toTypedArray().toFloatArray() + private val ambientUniform = FloatArray(3) + + init { + recomputeAmbient() + } + + override val sksl = """ +uniform float uTime; +uniform vec3 uResolution; +uniform vec3 uAmbient; + +const int MAX_COLORS = $colorCount; +uniform vec3 uColor[MAX_COLORS]; + +// Simplex 3D Noise +// by Ian McEwan, Ashima Arts +// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83 +// +vec4 permute(vec4 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0); + vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0 / 7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} + +vec4 main( vec2 fragCoord ) { + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * $scale - uResolution.xy) / mr; + + vec2 base = uv / 2; + + vec3 vColor = uColor[MAX_COLORS - 1]; + + const vec2 frequency = vec2(0.7, 0.3); + const float noiseFloor = 0.00001; + float t = uTime * 0.005; + + for(int i = 0; i < MAX_COLORS - 1; i++) { + float fi = float(i); + float flow = 5. + fi * 0.3; + float speed = 6. * $speed + fi * 0.3; + float seed = 1. + fi * 4.; + float noiseCeil = 0.6 + fi * 0.07; + + float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed))); + + vColor = mix(vColor, uColor[i], noise); + } + + vColor = max(vColor, uAmbient); + + // Elliptical falloff centred at the very top of the screen. + // Using fragCoord directly (pixels) and uResolution for screen size. + // Horizontal radius ~ 80 % of screen width → wide enough to cover corners. + // Vertical radius ~ 45 % of screen height → controls how far down the glow reaches. + vec2 topCenter = vec2(uResolution.x * 0.5, 0.0); + vec2 delta = fragCoord - topCenter; + vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65); + float normDist = length(delta / radii); + float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5); + + // Pre-multiplied alpha so the shader composites correctly over the dark background. + return vec4(vColor * alpha, alpha); +} + """ + + /** Updates the animated colors in-place without recreating the shader. */ + fun updateColors(colors: Array) { + colors.forEachIndexed { i, color -> + colorUniforms[i * 3 + 0] = color.red + colorUniforms[i * 3 + 1] = color.green + colorUniforms[i * 3 + 2] = color.blue + } + recomputeAmbient() + } + + private fun recomputeAmbient() { + val count = colorCount - 1 + var r = 0f + var g = 0f + var b = 0f + for (i in 0 until count) { + r += colorUniforms[i * 3] + g += colorUniforms[i * 3 + 1] + b += colorUniforms[i * 3 + 2] + } + val scale = 0.5f / count + ambientUniform[0] = r * scale + ambientUniform[1] = g * scale + ambientUniform[2] = b * scale + } + + override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height) + + runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms) + runtimeEffect.setFloatUniform( + name = "uAmbient", + value1 = ambientUniform[0], + value2 = ambientUniform[1], + value3 = ambientUniform[2], + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt new file mode 100644 index 0000000000..388914a11b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.shader + +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +interface TangemShader { + val speedModifier: Float + get() = 0.5f + + val sksl: String + + /** Applies the uniforms required for this shader to the effect */ + fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height) + runtimeEffect.setFloatUniform(name = "uTime", value1 = time) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt new file mode 100644 index 0000000000..899e3c8a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.shader.runtime + +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +internal class FallbackRuntimeEffect : RuntimeEffect { + override val isSupported: Boolean = false + override val isReady: Boolean = false + + override fun build(): Brush { + return Brush.horizontalGradient(listOf(Color.White, Color.White)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt new file mode 100644 index 0000000000..035495aad8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt @@ -0,0 +1,35 @@ +package com.tangem.core.ui.shader.runtime + +import android.os.Build +import androidx.compose.ui.graphics.Brush +import com.tangem.core.ui.shader.TangemShader + +interface RuntimeEffect { + + val isSupported: Boolean + val isReady: Boolean + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, values: FloatArray) {} + + fun update(shader: TangemShader, time: Float, width: Float, height: Float) {} + + fun build(): Brush +} + +internal fun buildEffect(shader: TangemShader): RuntimeEffect { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + RuntimeShaderEffect(shader) + } else { + FallbackRuntimeEffect() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt new file mode 100644 index 0000000000..78c50e2e7c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.shader.runtime + +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ShaderBrush +import com.tangem.core.ui.shader.TangemShader + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect { + private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl) + + override val isSupported: Boolean = true + override var isReady: Boolean = false + + override fun setFloatUniform(name: String, value1: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3) + } + + override fun setFloatUniform(name: String, values: FloatArray) { + compositeRuntimeEffect.setFloatUniform(name, values) + } + + override fun update(shader: TangemShader, time: Float, width: Float, height: Float) { + shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height) + isReady = width > 0 && height > 0 + } + + override fun build(): Brush { + return ShaderBrush(compositeRuntimeEffect) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt new file mode 100644 index 0000000000..d9b8583dcb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object AppBarWithSearchTestTags { + const val SEARCH_ICON = "APP_BAR_WITH_SEARCH_SEARCH_ICON" + const val TEXT_FIELD = "APP_BAR_WITH_SEARCH_TEXT_FIELD" +} \ 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 3ddbefea69..9cdf82f3f3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -16,5 +16,7 @@ object SwapTokenScreenTestTags { const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON" const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT" + const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT" + const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON" const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp new file mode 100644 index 0000000000..8844ed902d Binary files /dev/null and b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp differ diff --git a/core/ui/src/main/res/drawable/ic_arrow_back_28.xml b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml new file mode 100644 index 0000000000..3cfbdc0aa5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml b/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml new file mode 100644 index 0000000000..3a1012ac04 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_calendar_20.xml b/core/ui/src/main/res/drawable/ic_calendar_20.xml new file mode 100644 index 0000000000..46a4606330 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_calendar_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml new file mode 100644 index 0000000000..6ee9686437 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml b/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml new file mode 100644 index 0000000000..10e97276fb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_like_20.xml b/core/ui/src/main/res/drawable/ic_like_20.xml new file mode 100644 index 0000000000..b44b0f8818 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_like_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_share_new_24.xml b/core/ui/src/main/res/drawable/ic_share_new_24.xml new file mode 100644 index 0000000000..d24e87b696 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_share_new_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_shield_24.xml b/core/ui/src/main/res/drawable/ic_shield_24.xml new file mode 100644 index 0000000000..129f023c2e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_shield_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml new file mode 100644 index 0000000000..69f15ed1a6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp new file mode 100644 index 0000000000..9d18888c54 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp differ diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp index 2a808bbb73..0622453761 100644 Binary files a/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp and b/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp differ diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp index bf2b79a46f..1eb05e81c8 100644 Binary files a/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp and b/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp differ diff --git a/core/ui/src/main/res/drawable/shape_triangular.xml b/core/ui/src/main/res/drawable/shape_triangular.xml new file mode 100644 index 0000000000..c4baf11fb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/shape_triangular.xml @@ -0,0 +1,9 @@ + + + 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 fc62ab079a..5af224e026 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 @@ -4,9 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.account.converter.AccountConverterFactoryContainer -import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher import com.tangem.data.account.repository.AccountsExpandedDTO import com.tangem.data.account.repository.DefaultAccountsCRUDRepository @@ -25,7 +23,6 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.setTypes -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsExpandedRepository import com.tangem.domain.account.tokens.MainAccountTokensMigration @@ -43,12 +40,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object AccountDataModule { - @Provides - @Singleton - fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles { - return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager) - } - @Provides @Singleton fun provideAccountsCRUDRepository( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt index e283366b5b..deb207e821 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt @@ -1,8 +1,6 @@ package com.tangem.data.account.di import com.tangem.data.account.producer.AccountListCryptoCurrenciesProducer -import com.tangem.data.account.producer.DefaultMultiWalletCryptoCurrenciesProducer -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import dagger.Module import dagger.Provides @@ -17,10 +15,8 @@ internal object MultiWalletCryptoCurrenciesProducerModule { @Singleton @Provides fun provideMultiWalletCryptoCurrenciesProducerFactory( - accountsFeatureToggles: AccountsFeatureToggles, - defaultImpl: DefaultMultiWalletCryptoCurrenciesProducer.Factory, accountsImpl: AccountListCryptoCurrenciesProducer.Factory, ): MultiWalletCryptoCurrenciesProducer.Factory { - return if (accountsFeatureToggles.isFeatureEnabled) accountsImpl else defaultImpl + return accountsImpl } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt b/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt deleted file mode 100644 index 0a6accf03f..0000000000 --- a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.data.account.featuretoggle - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles - -internal class DefaultAccountsFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : AccountsFeatureToggles { - - override val isFeatureEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED") -} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index b75e956b6e..f7901863e9 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -10,6 +10,7 @@ import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.common.tokens.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code import com.tangem.datasource.api.common.response.ETAG_HEADER @@ -56,6 +57,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( private val mainAccountTokensMigration: DefaultMainAccountTokensMigration, ) : WalletAccountsFetcher, WalletAccountsSaver { + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() + override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse { val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull() val fetchResult = fetchWalletAccounts(userWalletId, savedAccountsResponse) @@ -90,7 +93,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) { val store = getAccountsResponseStore(userWalletId = userWalletId) - store.updateData { response } + store.updateData { response.applyTokensCompatibility() } } override suspend fun update( @@ -99,7 +102,9 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( ) { val store = getAccountsResponseStore(userWalletId = userWalletId) - store.updateData { transform(it) } + store.updateData { + transform(it).applyTokensCompatibility() + } } override suspend fun push( @@ -272,6 +277,22 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( ) } + private fun GetWalletAccountsResponse?.applyTokensCompatibility(): GetWalletAccountsResponse? { + if (this == null) return null + + return copy( + accounts = accounts.map { accountDTO -> + val tokens = accountDTO.tokens + + if (tokens.isNullOrEmpty()) return@map accountDTO + + accountDTO.copy( + tokens = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(tokens), + ) + }, + ) + } + private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore { return accountsResponseStoreFactory.create(userWalletId = userWalletId) } 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 795c6dbc37..3820a2e528 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 @@ -2,12 +2,11 @@ package com.tangem.data.account.producer 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.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.models.account.DerivationIndex +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer @@ -15,31 +14,29 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map /** * Implementation of [MultiWalletCryptoCurrenciesProducer] that produces crypto currencies of all accounts * - * @property params params - * @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 + * @property params params + * @property userWalletsListRepository repository for getting user wallets + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( @Assisted val params: MultiWalletCryptoCurrenciesProducer.Params, + private val singleAccountListSupplier: SingleAccountListSupplier, private val userWalletsListRepository: UserWalletsListRepository, - private val accountsResponseStoreFactory: AccountsResponseStoreFactory, - private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, override val flowProducerTools: FlowProducerTools, private val dispatchers: CoroutineDispatcherProvider, ) : MultiWalletCryptoCurrenciesProducer { override val fallback: Option> = emptySet().some() - @Suppress("NullableToStringCall") override fun produce(): Flow> { val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) @@ -47,23 +44,12 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet") } - return accountsResponseStoreFactory.create(userWalletId = userWallet.walletId).data - .distinctUntilChanged() - .map { response -> - if (response == null) return@map emptySet() - - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@map emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty(), - userWallet = userWallet, - accountIndex = accountIndex, - ) - } + return singleAccountListSupplier.invoke(params.userWalletId) + .map { accountList -> + accountList.accounts + .filterIsInstance() + .flatMapTo(hashSetOf(), Account.CryptoPortfolio::cryptoCurrencies) } - .onEmpty { emit(emptySet()) } .flowOn(dispatchers.default) } 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 a55c9c0da8..8dfe637ba5 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 @@ -5,8 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain 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.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 @@ -23,17 +21,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency * * @property demoConfig demo config * @property excludedBlockchains excluded blockchains - * @property userWalletsStore user wallets store - * @property userTokensResponseStore user tokens response store */ @Suppress("LongParameterList") internal class DefaultCardCryptoCurrencyFactory( private val demoConfig: DemoConfig, private val excludedBlockchains: ExcludedBlockchains, private val userWalletsListRepository: UserWalletsListRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, private val walletAccountsFetcher: WalletAccountsFetcher, - private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : CardCryptoCurrencyFactory { @@ -140,34 +134,21 @@ internal class DefaultCardCryptoCurrencyFactory( userWallet: UserWallet, networks: Set, ): Map> { - val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) + ?: return emptyMap() - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@flatMapTo emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty().filter { token -> - networks.any { - it.backendId == token.networkId && it.derivationPath.value == token.derivationPath - } - }, - userWallet = userWallet, - accountIndex = accountIndex, - ) - } - } else { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) - ?: return emptyMap() + val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() + ?: return@flatMapTo emptySet() responseCryptoCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> - networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } + tokens = accountDTO.tokens.orEmpty().filter { token -> + networks.any { + it.backendId == token.networkId && it.derivationPath.value == token.derivationPath + } }, userWallet = userWallet, - accountIndex = DerivationIndex.Main, + accountIndex = accountIndex, ) } .groupBy(CryptoCurrency::network) @@ -181,28 +162,17 @@ internal class DefaultCardCryptoCurrencyFactory( ): Map> { val networkIds = rawIds.map { it.toBlockchain().toNetworkId() } - return if (accountsFeatureToggles.isFeatureEnabled) { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) + ?: return emptyMap() - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@flatMapTo emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds }, - userWallet = userWallet, - accountIndex = accountIndex, - ) - } - } else { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) - ?: return emptyMap() + return response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() + ?: return@flatMapTo emptySet() responseCryptoCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> token.networkId in networkIds }, + tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds }, userWallet = userWallet, - accountIndex = DerivationIndex.Main, + accountIndex = accountIndex, ) } .groupBy { it.network.id.rawId } 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 cff0ae1d6d..a1ab6f180b 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 @@ -8,10 +8,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi 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.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 @@ -30,8 +27,6 @@ class UserTokensSaver( private val dispatchers: CoroutineDispatcherProvider, private val addressesEnricher: UserTokensResponseAddressesEnricher, private val walletServerBinder: WalletServerBinder, - private val appsFlyerStore: AppsFlyerStore, - private val accountsFeatureToggles: AccountsFeatureToggles, private val pushTokensRetryerPool: RetryerPool, ) { private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() @@ -68,22 +63,9 @@ class UserTokensSaver( return@withContext } - if (accountsFeatureToggles.isFeatureEnabled) { - val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) - pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) - } else { - val conversionData = appsFlyerStore.get() - - val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet.name.takeIf { it.isNotBlank() }, - walletType = WalletType.from(userWallet), - refcode = conversionData?.refcode, - campaign = conversionData?.campaign, - ) - - pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) - } + push(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) } suspend fun pushWithRetryer( @@ -103,14 +85,7 @@ class UserTokensSaver( ) } - private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) { - safeApiCall( - call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() }, - onError = { onFailSend() }, - ) - } - - private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { + private suspend fun push(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { safeApiCall( call = { val apiResponse = tangemTechApi.saveTokens( @@ -148,13 +123,7 @@ class UserTokensSaver( return this .enrichByAddress(userWalletId = userWalletId) - .let { response -> - if (accountsFeatureToggles.isFeatureEnabled) { - response.enrichByAccountId(userWalletId = userWalletId) - } else { - response - } - } + .enrichByAccountId(userWalletId = userWalletId) } private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse { 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 9ce3a6b121..2d0f7de535 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,7 +13,6 @@ 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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.walletmanager.WalletManagersFacade @@ -37,18 +36,14 @@ internal object DataCommonModule { fun provideCardCryptoCurrencyFactory( excludedBlockchains: ExcludedBlockchains, userWalletsListRepository: UserWalletsListRepository, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, - userTokensResponseStore: UserTokensResponseStore, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ): CardCryptoCurrencyFactory { return DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, userWalletsListRepository = userWalletsListRepository, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, - userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, ) } @@ -76,8 +71,6 @@ internal object DataCommonModule { dispatchers: CoroutineDispatcherProvider, addressesEnricher: UserTokensResponseAddressesEnricher, walletServerBinder: WalletServerBinder, - appsFlyerStore: AppsFlyerStore, - accountsFeatureToggles: AccountsFeatureToggles, ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, @@ -85,12 +78,10 @@ internal object DataCommonModule { userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, addressesEnricher = addressesEnricher, - accountsFeatureToggles = accountsFeatureToggles, pushTokensRetryerPool = RetryerPool( coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default), ), walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, ) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt b/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt index 54e69b23a1..f9c12f4d03 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt @@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse * Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens * in new application with new IDs */ -internal class UserTokensBackwardCompatibility { +class UserTokensBackwardCompatibility { fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse { return userTokensResponse.copy( @@ -28,6 +28,21 @@ internal class UserTokensBackwardCompatibility { ) } + fun applyCompatibilityAndGetUpdated(tokens: List): List { + return tokens.map { token -> + val oldSavedId = NETWORKS_TO_OLD_SAVED_IDS[token.networkId] + if (oldSavedId != null && token.id == oldSavedId) { + Blockchain.fromNetworkId(token.networkId)?.let { blockchain -> + token.copy( + id = blockchain.toCoinId(), + ) + } ?: token + } else { + token + } + } + } + companion object { private val NETWORKS_TO_OLD_SAVED_IDS = mapOf( "optimistic-ethereum" to "ethereum", 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 eb90a1efb0..749eadff8b 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 @@ -11,8 +11,8 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory 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.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -38,20 +38,16 @@ import org.junit.jupiter.params.ParameterizedTest internal class DefaultCardCryptoCurrencyFactoryTest { private val userWalletsListRepository: UserWalletsListRepository = mockk() - private val userTokensResponseStore: UserTokensResponseStore = mockk() private val excludedBlockchains = ExcludedBlockchains() - private val accountsFeatureToggles = mockk() private val walletAccountsFetcher = mockk() private val factory = DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, userWalletsListRepository = userWalletsListRepository, - userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory( networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains), ), - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) @@ -64,7 +60,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { @BeforeEach fun init() { - clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) + clearMocks(userWalletsListRepository, walletAccountsFetcher, iconUri) mockkStatic(Uri::class) every { Uri.parse(any()) } returns iconUri @@ -80,12 +76,11 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = createMultiWallet() val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - val userTokensResponse = model.userTokensResponse + val accountsResponse = model.accountsResponse val network = ethereum.network - every { accountsFeatureToggles.isFeatureEnabled } returns false every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse // Act val actual = factory.create(userWalletId = userWallet.walletId, network = network) @@ -97,25 +92,25 @@ internal class DefaultCardCryptoCurrencyFactoryTest { coVerifyOrder { userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) + walletAccountsFetcher.getSaved(userWalletId = userWallet.walletId) } } private fun provideTestModels() = listOf( - CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()), - CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()), + CreateTestModel.MultiWallet(accountsResponse = null, expected = emptyList()), + CreateTestModel.MultiWallet(accountsResponse = createAccountsResponse(), expected = emptyList()), CreateTestModel.MultiWallet( - userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)), + accountsResponse = createAccountsResponse(currencies = listOf(ethereum)), expected = listOf(ethereum), ), CreateTestModel.MultiWallet( - userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)), + accountsResponse = createAccountsResponse(listOf(element = bitcoin)), expected = emptyList(), ), ) - private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( + private fun createAccountsResponse(currencies: List = emptyList()): GetWalletAccountsResponse { + return createWalletAccountsResponse( currencies = currencies, isGroupedByNetwork = false, isSortedByBalance = false, @@ -150,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { } coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) + walletAccountsFetcher.getSaved(userWalletId = any()) } } @@ -195,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { } coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) + walletAccountsFetcher.getSaved(userWalletId = any()) } } @@ -218,7 +213,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { val expected: List data class MultiWallet( - val userTokensResponse: UserTokensResponse?, + val accountsResponse: GetWalletAccountsResponse?, override val expected: List, ) : CreateTestModel @@ -245,10 +240,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = model.multiWallet val networks = setOf(ethereum.network, bitcoin.network) - val userTokensResponse = model.userTokensResponse + val accountsResponse = model.accountsResponse - every { accountsFeatureToggles.isFeatureEnabled } returns false - coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse // Act val actual = runCatching { @@ -273,19 +267,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest { private fun provideTestModels() = listOf( CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = null, + accountsResponse = null, expected = Result.success(emptyMap()), ), CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = createUserTokensResponse(), + accountsResponse = createWalletAccountsResponse(emptyList(), false, false), expected = Result.success( setOf(ethereum.network, bitcoin.network).associateWith { emptyList() }, ), ), CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)), + accountsResponse = createWalletAccountsResponse(currencies = listOf(bitcoin, ethereum), false, false), expected = mapOf( bitcoin.network to listOf(bitcoin), ethereum.network to listOf(ethereum), @@ -293,12 +287,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest { ), CreateCurrenciesForMultiWalletModel( multiWallet = createSingleWallet(), - userTokensResponse = null, + accountsResponse = null, expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), ), CreateCurrenciesForMultiWalletModel( multiWallet = MockUserWalletFactory.createSingleWalletWithToken(), - userTokensResponse = null, + accountsResponse = null, expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), ), ) @@ -306,7 +300,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { data class CreateCurrenciesForMultiWalletModel( val multiWallet: UserWallet, - val userTokensResponse: UserTokensResponse?, + val accountsResponse: GetWalletAccountsResponse?, val expected: Result>>, ) @@ -546,11 +540,44 @@ internal class DefaultCardCryptoCurrencyFactoryTest { )!! } - private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( - currencies = currencies, - isGroupedByNetwork = false, - isSortedByBalance = false, + private fun createWalletAccountsResponse( + currencies: List, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): GetWalletAccountsResponse { + val tokens = currencies.map { currency -> + userTokensResponseFactory.createResponseToken(currency = currency, accountId = null) + } + + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = if (isGroupedByNetwork) { + UserTokensResponse.GroupType.NETWORK + } else { + UserTokensResponse.GroupType.NONE + }, + sort = if (isSortedByBalance) { + UserTokensResponse.SortType.BALANCE + } else { + UserTokensResponse.SortType.MANUAL + }, + totalAccounts = 1, + totalArchivedAccounts = 0, + ), + accounts = listOf( + WalletAccountDTO( + id = "account_0", + name = "Main", + derivationIndex = 0, + icon = "🏠", + iconColor = "#000000", + tokens = tokens, + totalTokens = tokens.size, + totalNetworks = currencies.map { it.network }.distinct().size, + ), + ), + unassignedTokens = emptyList(), ) } 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 16bb9deefe..4cc3f5aa9f 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 @@ -6,9 +6,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.api.tangemTech.models.WalletType -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -27,11 +25,7 @@ class UserTokensSaverTest { private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) private val enricher: UserTokensResponseAddressesEnricher = mockk() - private val accountsFeatureToggles = mockk { - every { this@mockk.isFeatureEnabled } returns true - } private val walletServerBinder: WalletServerBinder = mockk() - private val appsFlyerStore: AppsFlyerStore = mockk() private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, @@ -40,8 +34,6 @@ class UserTokensSaverTest { dispatchers = TestingCoroutineDispatcherProvider(), addressesEnricher = enricher, walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, pushTokensRetryerPool = mockk(), ) @@ -121,7 +113,6 @@ class UserTokensSaverTest { val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { accountsFeatureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt index 8c430ced24..9575c86449 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt @@ -3,6 +3,7 @@ package com.tangem.data.feedback.converters import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.domain.feedback.models.BlockchainInfo import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo import com.tangem.utils.converter.Converter @@ -16,22 +17,38 @@ import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainA internal object BlockchainInfoConverter : Converter { override fun convert(value: WalletManager): BlockchainInfo { - val derivationPath = value.wallet.publicKey.derivationPath + val wallet = value.wallet + val blockchain = wallet.blockchain + val derivationPath = wallet.publicKey.derivationPath return BlockchainInfo( - blockchain = value.wallet.blockchain.fullName, + blockchain = blockchain.fullName, derivationPath = derivationPath?.rawPath.orEmpty(), outputsCount = value.outputsCount?.toString(), host = value.currentHost, - addresses = value.wallet.mapAddresses(Address::value), - explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, - tokens = value.cardTokens.map { token -> - BlockchainInfo.TokenInfo( - id = token.id, - name = token.name, - contractAddress = token.contractAddress, - decimals = token.decimals.toString(), + addresses = wallet.mapAddresses(Address::value), + explorerLinks = wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, + tokens = buildList { + // add coin + add( + BlockchainInfo.TokenInfo( + id = blockchain.toCoinId(), + name = blockchain.getCoinName(), + contractAddress = wallet.address, + decimals = blockchain.decimals().toString(), + ), ) + // add other tokens + value.cardTokens.forEach { token -> + add( + BlockchainInfo.TokenInfo( + id = token.id, + name = token.name, + contractAddress = token.contractAddress, + decimals = token.decimals.toString(), + ), + ) + } }, ) } 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 7725874819..492194a851 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 @@ -6,8 +6,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.managetokens.utils.TokenAddressesConverter import com.tangem.datasource.api.common.response.getOrThrow @@ -26,7 +24,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -35,10 +32,8 @@ internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, - private val walletManagersFacade: WalletManagersFacade, private val excludedBlockchains: ExcludedBlockchains, private val dispatchers: CoroutineDispatcherProvider, - private val userTokensSaver: UserTokensSaver, private val networkFactory: NetworkFactory, ) : CustomTokensRepository { @@ -48,7 +43,6 @@ internal class DefaultCustomTokensRepository( ) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val userTokensResponseFactory = UserTokensResponseFactory() private val tokenAddressConverter = TokenAddressesConverter() override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = @@ -213,41 +207,6 @@ internal class DefaultCustomTokensRepository( ) } - @Deprecated("Use ManageCryptoCurrenciesUseCase") - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = - withContext(dispatchers.io) { - val cryptoCurrency = when (currency) { - is ManagedCryptoCurrency.Custom.Coin -> createCoin( - userWalletId = userWalletId, - networkId = currency.network.id, - derivationPath = currency.network.derivationPath, - ) - is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken( - network = currency.network, - rawId = currency.currencyId.rawCurrencyId, - name = currency.name, - symbol = currency.symbol, - decimals = currency.decimals, - contractAddress = currency.contractAddress, - ) - } - val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId) - - requireNotNull(storedCurrencies) { - "User tokens not found for user wallet [$userWalletId] while removing currency" - } - - val token = userTokensResponseFactory.createResponseToken(currency = cryptoCurrency, accountId = null) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }), - ) - when (cryptoCurrency) { - is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network)) - is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency)) - } - } - override suspend fun convertToCryptoCurrency( userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom, 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 98f8097fbc..28b44ce20f 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 @@ -7,10 +7,6 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -22,7 +18,6 @@ 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.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 @@ -48,24 +43,19 @@ import com.tangem.utils.coroutines.runSuspendCatching internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, - private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, private val testnetTokensStorage: TestnetTokensStorage, private val excludedBlockchains: ExcludedBlockchains, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, private val walletAccountsFetcher: WalletAccountsFetcher, - private val accountsFeatureToggles: AccountsFeatureToggles, networkFactory: NetworkFactory, ) : ManageTokensRepository { private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory( networkFactory = networkFactory, excludedBlockchains = excludedBlockchains, - accountsFeatureToggles = accountsFeatureToggles, ) - private val userTokensResponseFactory = UserTokensResponseFactory() // region getTokenListBatchFlow override fun getTokenListBatchFlow( @@ -92,10 +82,7 @@ internal class DefaultManageTokensRepository( val userWallet = request.params.userWalletId?.let(userWalletsListRepository::getSyncStrict) if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) { - when (val params = request.params) { - is ManageTokensListConfig.Account -> fetchTestnetCurrencies(userWallet, params) - is ManageTokensListConfig.Wallet -> fetchTestnetCurrenciesLegacy(userWallet, params) - } + fetchTestnetCurrencies(userWallet, request.params) } else { fetchCurrencies( userWallet = userWallet, @@ -139,24 +126,14 @@ internal class DefaultManageTokensRepository( coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) }, ) - val items = when (val params = request.params) { - is ManageTokensListConfig.Account -> createManagedCryptoCurrencyList( - params = params, - userWallet = userWallet, - isFirstBatchFetching = isFirstBatchFetching, - loadUserTokensFromRemote = loadUserTokensFromRemote, - query = query, - updatedCoinsResponse = updatedCoinsResponse, - ) - is ManageTokensListConfig.Wallet -> createManagedCryptoCurrencyListLegacy( - params = params, - userWallet = userWallet, - isFirstBatchFetching = isFirstBatchFetching, - loadUserTokensFromRemote = loadUserTokensFromRemote, - query = query, - updatedCoinsResponse = updatedCoinsResponse, - ) - } + val items = createManagedCryptoCurrencyList( + params = request.params, + userWallet = userWallet, + isFirstBatchFetching = isFirstBatchFetching, + loadUserTokensFromRemote = loadUserTokensFromRemote, + query = query, + updatedCoinsResponse = updatedCoinsResponse, + ) return BatchFetchResult.Success( data = items, @@ -167,7 +144,7 @@ internal class DefaultManageTokensRepository( @Suppress("CyclomaticComplexMethod") private suspend fun createManagedCryptoCurrencyList( - params: ManageTokensListConfig.Account, + params: ManageTokensListConfig, userWallet: UserWallet?, isFirstBatchFetching: Boolean, loadUserTokensFromRemote: Boolean, @@ -238,56 +215,9 @@ internal class DefaultManageTokensRepository( return items } - private suspend fun createManagedCryptoCurrencyListLegacy( - params: ManageTokensListConfig.Wallet, - userWallet: UserWallet?, - isFirstBatchFetching: Boolean, - loadUserTokensFromRemote: Boolean, - query: String?, - updatedCoinsResponse: CoinsResponse, - ): List { - val tokensResponse = params.userWalletId?.let { userWalletId -> - if (loadUserTokensFromRemote && userWallet != null) { - safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - // save tokens response only if loadUserTokensFromRemote is true and it means onboarding call - createAndSaveDefaultUserTokensResponse(userWallet = userWallet) - } - } else { - getSavedUserTokensResponseSync(userWalletId) - } - } - - val isCreateWithCustom = isFirstBatchFetching && - tokensResponse != null && - userWallet != null && - query == null - - return if (isCreateWithCustom) { - managedCryptoCurrencyFactory.createWithCustomTokens( - coinsResponse = updatedCoinsResponse, - tokensResponse = tokensResponse, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } else { - managedCryptoCurrencyFactory.create( - coinsResponse = updatedCoinsResponse, - tokensResponse = tokensResponse, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - } - - private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { - val userTokensResponse = createDefaultUserTokensResponse(userWallet) - userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false) - return userTokensResponse - } - private suspend fun fetchTestnetCurrencies( userWallet: UserWallet, - params: ManageTokensListConfig.Account, + params: ManageTokensListConfig, ): BatchFetchResult.Success> { val searchText = params.searchText val testnetTokensConfig = testnetTokensStorage.getConfig() @@ -342,45 +272,6 @@ internal class DefaultManageTokensRepository( ) } - private suspend fun fetchTestnetCurrenciesLegacy( - userWallet: UserWallet, - params: ManageTokensListConfig.Wallet, - ): BatchFetchResult.Success> { - val searchText = params.searchText - val testnetTokensConfig = testnetTokensStorage.getConfig() - - val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens( - testnetTokensConfig = if (!searchText.isNullOrBlank()) { - testnetTokensConfig.copy( - tokens = testnetTokensConfig.tokens.filter { token -> - token.symbol.contains(other = searchText, ignoreCase = true) || - token.name.contains(other = searchText, ignoreCase = true) - }, - ) - } else { - testnetTokensConfig - }, - tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId), - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - - return BatchFetchResult.Success( - data = items, - empty = items.isEmpty(), - last = true, - ) - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet) = - userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( - userWallet = userWallet, - ), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - private fun getSupportedBlockchains(userWallet: UserWallet?): List { return userWallet?.supportedBlockchains(excludedBlockchains) ?: Blockchain.entries.filter { 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 ea538bf3d2..01922b4996 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 @@ -2,8 +2,6 @@ package com.tangem.data.managetokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository @@ -11,11 +9,9 @@ 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.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 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -34,13 +30,10 @@ internal object ManageTokensDataModule { userWalletsListRepository: UserWalletsListRepository, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, - userTokensSaver: UserTokensSaver, testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, - cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, networkFactory: NetworkFactory, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, ): ManageTokensRepository { return DefaultManageTokensRepository( @@ -48,13 +41,10 @@ internal object ManageTokensDataModule { userWalletsListRepository = userWalletsListRepository, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, - userTokenSaver = userTokensSaver, testnetTokensStorage = testnetTokensStorage, excludedBlockchains = excludedBlockchains, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, networkFactory = networkFactory, dispatchers = dispatchers, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) } @@ -65,20 +55,16 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, - walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, - userTokensSaver: UserTokensSaver, networkFactory: NetworkFactory, ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi = tangemTechApi, userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, - walletManagersFacade = walletManagersFacade, excludedBlockchains = excludedBlockchains, dispatchers = dispatchers, - userTokensSaver = userTokensSaver, networkFactory = networkFactory, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 22beb60476..efdfc59129 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,7 +13,6 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork @@ -21,7 +20,6 @@ import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.BlockchainUtils import timber.log.Timber @@ -29,7 +27,6 @@ import timber.log.Timber internal class ManagedCryptoCurrencyFactory( private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, - private val accountsFeatureToggles: AccountsFeatureToggles, ) { fun create( @@ -120,30 +117,15 @@ internal class ManagedCryptoCurrencyFactory( ?.takeUnless { it in excludedBlockchains } ?: return null - val network = if (accountsFeatureToggles.isFeatureEnabled) { - val network = networkFactory.create( - blockchain = blockchain, - extraDerivationPath = token.derivationPath, - userWallet = userWallet, - accountIndex = accountIndex, - ) ?: return null + val network = networkFactory.create( + blockchain = blockchain, + extraDerivationPath = token.derivationPath, + userWallet = userWallet, + accountIndex = accountIndex, + ) ?: return null - if (!checkIsCustomToken(token, network.derivationPath)) { - return null - } - - network - } else { - if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) { - return null - } - - networkFactory.create( - blockchain = blockchain, - extraDerivationPath = token.derivationPath, - userWallet = userWallet, - accountIndex = accountIndex, - ) ?: return null + if (!checkIsCustomToken(token, network.derivationPath)) { + return null } val contractAddress = token.contractAddress @@ -285,23 +267,10 @@ internal class ManagedCryptoCurrencyFactory( return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" } - private fun checkIsCustomToken( - token: UserTokensResponse.Token, - blockchain: Blockchain, - derivationStyleProvider: DerivationStyleProvider, - ): Boolean = token.id.isNullOrBlank() || - checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider) - private fun checkIsCustomToken(token: UserTokensResponse.Token, derivationPath: Network.DerivationPath): Boolean { return token.id.isNullOrBlank() || derivationPath is Network.DerivationPath.Custom } - private fun checkIsCustomDerivationPath( - derivationPath: String?, - blockchain: Blockchain, - derivationStyleProvider: DerivationStyleProvider, - ): Boolean = derivationPath != blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath - /** * Filter tokens for TerraV1 (Terra Classic) network. * Only native coin (LUNC) and TerraClassicUSD (USTC) are allowed. diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 1131c43dfc..7961e06c77 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -13,13 +13,8 @@ import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.File @@ -67,6 +62,8 @@ internal class DefaultNetworksStatusesStore( override fun get(userWalletId: UserWalletId): Flow> { return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + .adaptiveThrottle() + .conflate() } override suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? { @@ -180,4 +177,61 @@ internal class DefaultNetworksStatusesStore( } } } +} + +@Suppress("MagicNumber") +internal fun Flow>.adaptiveThrottle(): Flow> = channelFlow { + var accumulator: Set? = null + var lastEmitTime = 0L + + // params that control maximum emissions that can be throttled + var densityLevel = 0 + val maxDensity = 10 + + // params that control maximum delay and growth of delay between emissions + var lastDelay = 0L + val maxDelay = 1500L + val growthFactor = 250L + + fun resetThrottling() { + lastDelay = 0L + densityLevel = 0 + } + + this@adaptiveThrottle.collectLatest { newSet -> + val previousSet: Collection? = accumulator + accumulator = newSet + + when { + // first value, just emit + previousSet == null -> resetThrottling() + // changed size, just emit + previousSet.size != newSet.size -> resetThrottling() + + // apply adaptive throttling + else -> { + val networksCount = newSet.size + // more networks - more throttling + val cooldownThreshold = when { + networksCount in 10..25 -> 300L + networksCount > 25 -> 500L + // 0..9 networks + else -> 100L + } + + val now = System.currentTimeMillis() + val timeSinceLastEmit = now - lastEmitTime + if (timeSinceLastEmit < cooldownThreshold && densityLevel < maxDensity) { + lastDelay = (lastDelay + growthFactor).coerceAtMost(maximumValue = maxDelay) + densityLevel += 1 + delay(lastDelay) + } else { + resetThrottling() + } + } + } + + lastEmitTime = System.currentTimeMillis() + channel.send(newSet) + } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt new file mode 100644 index 0000000000..719df24b15 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt @@ -0,0 +1,109 @@ +package com.tangem.data.networks.store + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +internal class StoreAdaptiveThrottleTest { + + @Test + fun `first value is emitted immediately`() = runTest { + val flow = flowOf(setOf(1, 2, 3)).adaptiveThrottle() + + flow.test { + val item = awaitItem() + assertThat(item).isEqualTo(setOf(1, 2, 3)) + awaitComplete() + } + } + + @Test + fun `size change bypasses throttling`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + assertThat(awaitItem()).isEqualTo(setOf(1, 2)) + + upstream.emit(setOf(1, 2, 3)) + assertThat(awaitItem()).isEqualTo(setOf(1, 2, 3)) + + upstream.emit(setOf(1)) + assertThat(awaitItem()).isEqualTo(setOf(1)) + } + } + + @Test + fun `same size events trigger throttling delay`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + upstream.emit(setOf(3, 4)) + + // delay should happen + expectNoEvents() + advanceTimeBy(250) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(3, 4)) + } + } + + @Test + fun `rapid events result in only latest emission due to collectLatest`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + launch { + upstream.emit(setOf(3, 4)) + upstream.emit(setOf(5, 6)) + upstream.emit(setOf(7, 8)) + } + + // delay should happen + expectNoEvents() + advanceTimeBy(250) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(7, 8)) + } + } + + @Test + fun `throttling resets when cooldown window passed`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + upstream.emit(setOf(3, 4)) + expectNoEvents() + advanceTimeBy(250) + awaitItem() + + // wait long enough to reset throttling + advanceTimeBy(2000) + + upstream.emit(setOf(5, 6)) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(5, 6)) + } + } +} \ No newline at end of file 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 6501990863..735a95f01e 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 @@ -17,8 +17,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse 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.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 @@ -43,7 +41,6 @@ import timber.log.Timber * * @property excludedBlockchains excluded blockchains * @property hotCryptoResponseStore store of `HotCryptoResponse` - * @property userWalletsStore store of `UserWallet` * @property tangemTechApi tangem tech api * @property appCurrencyResponseStore store of current app currency * @property dispatchers dispatchers @@ -59,8 +56,6 @@ internal class DefaultHotCryptoRepository( private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val userTokensResponseStore: UserTokensResponseStore, private val walletAccountsFetcher: WalletAccountsFetcher, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -106,12 +101,8 @@ internal class DefaultHotCryptoRepository( private fun getWalletsWithTokensFlow(): Flow>> { return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> - if (accountsFeatureToggles.isFeatureEnabled) { - walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } - } else { - userTokensResponseStore.get(userWalletId = userWallet.walletId) - } - .map { userWallet to it?.tokens.orEmpty() } + walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } + .map { userWallet to it.tokens } } combine(flows) { it.toMap() } 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 88a6e8f021..ca1ca2f6fc 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 @@ -18,7 +18,7 @@ import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -27,8 +27,6 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore 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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -110,8 +108,6 @@ internal object OnrampDataModule { appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, - userTokensResponseStore: UserTokensResponseStore, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, ): HotCryptoRepository { return DefaultHotCryptoRepository( @@ -122,8 +118,6 @@ internal object OnrampDataModule { appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, - userTokensResponseStore = userTokensResponseStore, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) } @@ -131,11 +125,11 @@ internal object OnrampDataModule { @Provides @Singleton fun provideMercuryoRepository( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchersProvider: CoroutineDispatcherProvider, ): LegacyTopUpRepository { return MercuryoTopUpRepository( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, dispatchersProvider = dispatchersProvider, ) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt index a2b80fec76..9eaee443e1 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt @@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -13,14 +12,13 @@ import kotlinx.coroutines.withContext import javax.inject.Inject internal class MercuryoTopUpRepository @Inject constructor( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val dispatchersProvider: CoroutineDispatcherProvider, ) : LegacyTopUpRepository { override suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String = withContext(dispatchersProvider.default) { val blockchain = cryptoCurrency.network.toBlockchain() - val environmentConfig = environmentConfigStorage.getConfigSync() val builder = Uri.Builder() .scheme(LegacyTopUpRepository.SCHEME) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt index 5eedc18e16..171c5c720a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt @@ -49,7 +49,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import timber.log.Timber @@ -161,10 +163,6 @@ internal class DefaultStakeKitRepository( private fun getAvailableStakeKitIntegrationsIds(): List { return StakingIntegrationID.StakeKit.entries - // load all integrations for now and filter in use cases if needed - // .filterNot { - // it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled - // } } private fun NetworkTypeDTO.extractJsonName(): String { 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 bdb663a190..b46d6ea594 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 @@ -8,7 +8,6 @@ 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 import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -17,7 +16,6 @@ import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.lib.crypto.BlockchainUtils.isCardano import com.tangem.lib.crypto.BlockchainUtils.isSolana @@ -35,7 +33,6 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val walletManagersFacade: WalletManagersFacade, ) : StakingRepository { override fun getStakingAvailability( @@ -43,7 +40,7 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, ): Flow { return channelFlow { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { send(StakingAvailability.Unavailable) return@channelFlow } @@ -79,7 +76,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { return StakingAvailability.Unavailable } @@ -119,31 +116,14 @@ internal class DefaultStakingRepository( } } - private suspend fun checkFeatureToggleEnabled(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean { return when (cryptoCurrency.network.id.toBlockchain()) { - Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled Blockchain.Ethereum -> { when (cryptoCurrency) { is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled is CryptoCurrency.Token -> true } } - Blockchain.Cardano -> { - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val balance = stakingBalanceStoreV2.getSyncOrNull( - userWalletId = userWalletId, - stakingId = StakingID( - integrationId = StakingIntegrationID.create(currencyId = cryptoCurrency.id)?.value - ?: return false, - address = address, - ), - ) - if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) { - return true - } else { - stakingFeatureToggles.isCardanoStakingEnabled - } - } else -> true } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index f1c17a770a..0aa8abf1b5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -61,7 +61,6 @@ internal object StakingDataModule { dispatchers: CoroutineDispatcherProvider, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, - walletManagersFacade: WalletManagersFacade, ): StakingRepository { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, @@ -69,7 +68,6 @@ internal object StakingDataModule { stakingBalanceStoreV2 = stakeKitBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, - walletManagersFacade = walletManagersFacade, stakingFeatureToggles = stakingFeatureToggles, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt index e5ee6c623a..fe7e688a8f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -7,12 +7,6 @@ internal class DefaultStakingFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : StakingFeatureToggles { - override val isTonStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED") - - override val isCardanoStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("STAKING_CARDANO_ENABLED") - override val isEthStakingEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("STAKING_ETH_ENABLED") } \ No newline at end of file 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 3734069fb2..37c6ab2dfb 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 @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId @@ -30,7 +29,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn internal class DefaultSwapTransactionRepository( @@ -39,7 +37,6 @@ internal class DefaultSwapTransactionRepository( private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, private val multiAccountListSupplier: MultiAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : SwapTransactionRepository { private val listConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -122,11 +119,7 @@ internal class DefaultSwapTransactionRepository( flow2 = appPreferencesStore.getObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), - flow3 = if (accountsFeatureToggles.isFeatureEnabled) { - multiAccountListSupplier() - } else { - flowOf(emptyList()) - }, + flow3 = multiAccountListSupplier(), ) { savedTransactions, txStatuses, multiAccountList -> val currencyTxs = savedTransactions ?.filter { swapTxList -> diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index fd3fadb12b..6fee629eea 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -12,7 +12,6 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.express.ExpressRepository import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher @@ -71,7 +70,6 @@ internal object SwapDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, multiAccountListSupplier: MultiAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( @@ -79,7 +77,6 @@ internal object SwapDataModule { responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, networkFactory = networkFactory, multiAccountListSupplier = multiAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, dispatchers = dispatchers, ) } 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 deleted file mode 100644 index 4b563eb468..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.data.tokens - -import arrow.core.Either -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver -import com.tangem.data.tokens.utils.CustomTokensMerger -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.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 -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 -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import timber.log.Timber - -/** - * Default implementation of [MultiWalletCryptoCurrenciesFetcher] - * - * @property tangemTechApi Tangem Tech API - * @property userTokensResponseStore store of [UserTokensResponse] - * @property userTokensSaver user tokens saver - * @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card - * @property expressServiceFetcher express service loader - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class DefaultMultiWalletCryptoCurrenciesFetcher( - private val demoConfig: DemoConfig, - private val userWalletsListRepository: UserWalletsListRepository, - private val tangemTechApi: TangemTechApi, - private val customTokensMerger: CustomTokensMerger, - private val userTokensResponseStore: UserTokensResponseStore, - private val userTokensSaver: UserTokensSaver, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val expressServiceFetcher: ExpressServiceFetcher, - private val dispatchers: CoroutineDispatcherProvider, -) : MultiWalletCryptoCurrenciesFetcher { - - private val userTokensResponseFactory = UserTokensResponseFactory() - - override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { - val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) - - if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") - - val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) { - createDefaultUserTokensResponse(userWallet = userWallet) - } else { - safeApiCall( - call = { - withContext(dispatchers.io) { - tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind() - } - }, - onError = { - handleFetchTokensError(error = it, userWallet = userWallet) - }, - ) - } - - val compatibleUserTokensResponse = response - .let { it.copy(tokens = it.tokens.distinct()) } - .let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) } - - userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse) - - fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse) - } - - private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean { - val isDemoCard = demoConfig.isDemoCardId(cardId = cardId) - - return if (isDemoCard) { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId) - - response == null - } else { - false - } - } - - private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse { - val userWalletId = userWallet.walletId - - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - ?: createDefaultUserTokensResponse(userWallet = userWallet) - - if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) { - Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId") - - userTokensSaver.push(userWalletId, response) - } - - return response - } - - private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { - val tokens = userTokens.tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - - expressServiceFetcher.fetch(userWallet = userWallet, assetIds = tokens) - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet), - isGroupedByNetwork = false, - isSortedByBalance = false, - accountId = null, - ) - } -} \ No newline at end of file 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 e3e6f70446..68065f148b 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 @@ -1,16 +1,8 @@ package com.tangem.data.tokens.di import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher -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.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 import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -27,39 +19,16 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { @Singleton @Provides fun provideMultiWalletCryptoCurrenciesFetcher( - accountsFeatureToggles: AccountsFeatureToggles, - tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, - userTokensResponseStore: UserTokensResponseStore, - userTokensSaver: UserTokensSaver, - cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - expressServiceFetcher: ExpressServiceFetcher, walletAccountsFetcher: WalletAccountsFetcher, + expressServiceFetcher: ExpressServiceFetcher, dispatchers: CoroutineDispatcherProvider, ): MultiWalletCryptoCurrenciesFetcher { - return if (accountsFeatureToggles.isFeatureEnabled) { - AccountListCryptoCurrenciesFetcher( - userWalletsListRepository = userWalletsListRepository, - walletAccountsFetcher = walletAccountsFetcher, - expressServiceFetcher = expressServiceFetcher, - dispatchers = dispatchers, - ) - } else { - DefaultMultiWalletCryptoCurrenciesFetcher( - demoConfig = DemoConfig, - userWalletsListRepository = userWalletsListRepository, - tangemTechApi = tangemTechApi, - customTokensMerger = CustomTokensMerger( - tangemTechApi = tangemTechApi, - userTokensSaver = userTokensSaver, - dispatchers = dispatchers, - ), - userTokensResponseStore = userTokensResponseStore, - userTokensSaver = userTokensSaver, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - expressServiceFetcher = expressServiceFetcher, - dispatchers = dispatchers, - ) - } + return AccountListCryptoCurrenciesFetcher( + userWalletsListRepository = userWalletsListRepository, + walletAccountsFetcher = walletAccountsFetcher, + expressServiceFetcher = expressServiceFetcher, + dispatchers = dispatchers, + ) } } \ No newline at end of file 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 f30629e0d4..2f577c7b2b 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 @@ -4,7 +4,6 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository @@ -13,7 +12,6 @@ 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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier @@ -22,7 +20,6 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -46,10 +43,8 @@ internal object TokensDataModule { expressServiceFetcher: ExpressServiceFetcher, excludedBlockchains: ExcludedBlockchains, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - tokensSaver: UserTokensSaver, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - accountsFeatureToggles: AccountsFeatureToggles, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, @@ -61,10 +56,8 @@ internal object TokensDataModule { dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - userTokensSaver = tokensSaver, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - accountsFeatureToggles = accountsFeatureToggles, ) } @@ -73,12 +66,10 @@ internal object TokensDataModule { fun provideCurrencyChecksRepository( walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, - sendFeatureToggles: SendFeatureToggles, ): CurrencyChecksRepository { return DefaultCurrencyChecksRepository( walletManagersFacade = walletManagersFacade, coroutineDispatchers = coroutineDispatcherProvider, - sendFeatureToggles = sendFeatureToggles, ) } 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 b2ec4a4968..952b4aa680 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 @@ -2,41 +2,44 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionStatus -import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison -import com.tangem.blockchainsdk.utils.* -import com.tangem.data.common.api.safeApiCall +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.common.currency.* -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.getTokenId 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.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 import com.tangem.domain.express.models.ExpressAsset 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.network.Network -import com.tangem.domain.models.wallet.* +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.* +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -49,150 +52,13 @@ internal class DefaultCurrenciesRepository( private val expressServiceFetcher: ExpressServiceFetcher, private val dispatchers: CoroutineDispatcherProvider, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, - private val accountsFeatureToggles: AccountsFeatureToggles, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { - private val demoConfig = DemoConfig private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val userTokensResponseFactory = UserTokensResponseFactory() - private val customTokensMerger = CustomTokensMerger( - tangemTechApi = tangemTechApi, - dispatchers = dispatchers, - userTokensSaver = userTokensSaver, - ) - - override suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) = withContext(dispatchers.io) { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) - - val response = userTokensResponseFactory.createUserTokensResponse( - currencies = currencies, - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - userTokensSaver.storeAndPush(userWalletId, response) - } - - override suspend fun addCurrenciesCache( - userWalletId: UserWalletId, - currencies: List, - ): List = withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) - - val currenciesToAdd = filterAlreadyAddedCurrencies( - savedCurrencies = savedCurrencies.tokens, - currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies), - ) - - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken), - ) - - userTokensSaver.store( - userWalletId = userWalletId, - response = updatedResponse, - ) - - fetchExpressAssetsByNetworkIds( - userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId), - userTokens = updatedResponse, - ) - - currenciesToAdd - } - - private fun filterAlreadyAddedCurrencies( - savedCurrencies: List, - currenciesToAdd: List, - ): List { - return currenciesToAdd.filter { currency -> - val networkId = currency.network.toBlockchain().toNetworkId() - val contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress - - savedCurrencies.none { token -> - token.contractAddress == contractAddress && - token.networkId == networkId && - token.derivationPath == currency.network.derivationPath.value - } - } - } - - private fun populateCurrenciesWithMissedCoins(currencies: List): List { - val currenciesSequence = currencies.asSequence() - - val networksWithTokens = currenciesSequence - .filterIsInstance() - .map { it.network } - .distinct() - - val networksWithCoins = currenciesSequence - .filterIsInstance() - .map { it.network } - .distinct() - - val networksNeedingCoins = (networksWithTokens - networksWithCoins.toSet()).toMutableList() - - if (networksNeedingCoins.isEmpty()) return currencies - - return buildList { - currencies.forEach { currency -> - if (currency is CryptoCurrency.Token && currency.network in networksNeedingCoins) { - val coin = cryptoCurrencyFactory.createCoin(currency.network) - add(coin) - - networksNeedingCoins.remove(currency.network) - } - - add(currency) - } - } - } - - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = - withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, - ) - - val token = userTokensResponseFactory.createResponseToken(currency) - val updatedResponse = - savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = updatedResponse, - ) - } - - override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { - return withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, - ) - - val tokens = currencies.map(userTokensResponseFactory::createResponseToken) - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filterNot(tokens::contains), - ) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = updatedResponse, - ) - } - } override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { @@ -288,47 +154,12 @@ internal class DefaultCurrenciesRepository( } } - override suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean, - ): List = withContext(dispatchers.io) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - - fetchTokensIfCacheExpired(userWallet, refresh) - - val storedTokens = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWallet.walletId), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - - responseCryptoCurrenciesFactory.createCurrencies( - response = storedTokens, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - override suspend fun getNetworkCoin( userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - return if (accountsFeatureToggles.isFeatureEnabled) { - getNetworkCoinNew(userWalletId, networkId, derivationPath) - } else { - getNetworkCoinLegacy(userWalletId, networkId, derivationPath) - } - } - - private suspend fun getNetworkCoinNew( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin = withContext(dispatchers.default) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), ) .orEmpty() @@ -340,76 +171,6 @@ internal class DefaultCurrenciesRepository( ?: error("Unable to find coin for network ID: $networkId") } - private suspend fun getNetworkCoinLegacy( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { - return withContext(dispatchers.io) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) - - fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - - val storedTokens = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - val blockchain = networkId.toBlockchain() - val blockchainNetworkId = blockchain.toNetworkId() - val coinId = blockchain.toCoinId() - - val storedCoin = storedTokens.tokens - .find { token -> - token.networkId == blockchainNetworkId && - compareIdWithMigrations(token, coinId) && - token.derivationPath == derivationPath.value - } ?: error("Coin in this network $networkId not found") - - val coin = responseCryptoCurrenciesFactory.createCurrency( - responseToken = storedCoin, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - - coin as? CryptoCurrency.Coin ?: error("Unable to create currency") - } - } - - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return channelFlow { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - if (userWallet.isMultiCurrency) { - getSavedUserTokensResponse(userWalletId) - .map { response -> response.group == UserTokensResponse.GroupType.NETWORK } - .distinctUntilChanged() - .onEach { isGrouped -> send(isGrouped) } - .launchIn(scope = this + dispatchers.io) - } else { - send(element = false) - } - } - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return channelFlow { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - if (userWallet.isMultiCurrency) { - getSavedUserTokensResponse(userWalletId) - .map { response -> response.sort == UserTokensResponse.SortType.BALANCE } - .distinctUntilChanged() - .onEach { isSorted -> send(isSorted) } - .launchIn(scope = this + dispatchers.io) - } else { - send(element = false) - } - } - } - override suspend fun isSendBlockedByPendingTransactions( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -511,85 +272,11 @@ internal class DefaultCurrenciesRepository( ) ?: error("Unable to create token") } - @OptIn(ExperimentalCoroutinesApi::class) - override fun getAllWalletsCryptoCurrencies( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> - - userWallets.filter { it.isMultiCurrency } - .forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } - - val userWalletsWithCurrencies = userWallets - .filterNot(UserWallet::isLocked) - .map { userWallet -> - getCurrenciesForWallet(userWallet, currencyRawId).map { userWallet to it } - } - - combine(userWalletsWithCurrencies) { it.toMap() } - .onEmpty { emit(value = emptyMap()) } - } - } - - @Suppress("SuspendFunWithFlowReturnType") - private suspend fun getCurrenciesForWallet( - userWallet: UserWallet, - currencyRawId: CryptoCurrency.RawID, - ): Flow> { - return when { - userWallet.isMultiCurrency -> { - getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> - val filterResponse = storedTokens.tokens.filter { - getL2CompatibilityTokenComparison(it, currencyRawId.value) - } - - responseCryptoCurrenciesFactory.createCurrencies( - response = storedTokens.copy(tokens = filterResponse), - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - } - - else -> { - val currencies = - if (userWallet.requireColdWallet().scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId) - } else { - val currency = - getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) - - if (currency.id.rawCurrencyId == currencyRawId) { - listOf(currency) - } else { - emptyList() - } - } - flow { - emit(currencies) - } - } - } - } - override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.backendId) return blockchain?.isNetworkFeeZero() == true } - override suspend fun syncTokens(userWalletId: UserWalletId) { - runSuspendCatching { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = savedCurrencies, - ) - } - } - override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? { return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver } @@ -604,60 +291,6 @@ internal class DefaultCurrenciesRepository( } } - private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { - cacheRegistry.invokeOnExpire( - key = getTokensCacheKey(userWallet.walletId), - skipCache = refresh, - block = { fetchTokens(userWallet) }, - ) - } - - private fun compareIdWithMigrations(token: UserTokensResponse.Token, coinId: String): Boolean { - return when { - token.id == OLD_POLYGON_NAME -> NEW_POLYGON_NAME == coinId - else -> token.id == coinId - } - } - - private suspend fun fetchTokens(userWallet: UserWallet) { - val userWalletId = userWallet.walletId - - val response = if (userWallet is UserWallet.Cold && checkIsEmptyDemoWallet(userWallet)) { - createDefaultUserTokensResponse(userWallet) - } else { - safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - handleFetchTokensError(userWallet, it) - } - } - - val compatibleUserTokensResponse = response - .let { it.copy(tokens = it.tokens.distinct()) } - .let { customTokensMerger.mergeIfPresented(userWalletId, it) } - - userTokensSaver.store(userWalletId, compatibleUserTokensResponse) - - fetchExpressAssetsByNetworkIds(userWallet, compatibleUserTokensResponse) - } - - private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet.Cold): Boolean { - val response = getSavedUserTokensResponseSync(key = userWallet.walletId) - - return demoConfig.isDemoCardId(userWallet.cardId) && response == null - } - - private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { - val tokens = userTokens.tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - - coroutineScope { - launch { expressServiceFetcher.fetch(userWallet, tokens) } - } - } - private suspend fun fetchExpressAssetsByNetworkIds( userWallet: UserWallet, cryptoCurrencies: List, @@ -683,41 +316,6 @@ internal class DefaultCurrenciesRepository( private fun getAssetsCacheKey(userWalletId: UserWalletId): String = "assets_cache_key_${userWalletId.stringValue}" - private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { - val userWalletId = userWallet.walletId - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - ?: createDefaultUserTokensResponse(userWallet = userWallet) - - if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { - Timber.w( - e, - "Requested currencies could not be found in the remote store for: $userWalletId", - ) - - userTokensSaver.push(userWalletId, response) - } else { - cacheRegistry.invalidate(getTokensCacheKey(userWalletId)) - } - - return response - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet) = - userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( - userWallet = userWallet, - ), - isGroupedByNetwork = false, - isSortedByBalance = false, - accountId = null, - ) - - private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) - } - private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) { val userWalletId = userWallet.walletId @@ -741,13 +339,7 @@ internal class DefaultCurrenciesRepository( } } - private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" - private fun getSavedUserTokensResponse(key: UserWalletId): Flow { return userTokensResponseStore.get(userWalletId = key).filterNotNull() } - - private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { - return userTokensResponseStore.getSyncOrNull(userWalletId = key) - } } \ No newline at end of file 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 b1ecc1457c..b7d34b0d39 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 @@ -15,7 +15,6 @@ import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero @@ -25,7 +24,6 @@ import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( private val walletManagersFacade: WalletManagersFacade, private val coroutineDispatchers: CoroutineDispatcherProvider, - private val sendFeatureToggles: SendFeatureToggles, ) : CurrencyChecksRepository { override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { @@ -67,7 +65,6 @@ internal class DefaultCurrencyChecksRepository( } override fun isNetworkSupportedForGaslessTx(network: Network): Boolean { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) return false val blockchain = Blockchain.fromId(network.rawId) return blockchain.isGaslessTxSupported } 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 deleted file mode 100644 index 1b649ebbe0..0000000000 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ /dev/null @@ -1,549 +0,0 @@ -package com.tangem.data.tokens - -import arrow.core.left -import arrow.core.right -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.common.response.ApiResponse -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.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.demo.models.DemoConfig -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.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -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 -import org.junit.jupiter.api.TestInstance - -/** -[REDACTED_AUTHOR] - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { - - private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() - private val userTokensResponseFactory = UserTokensResponseFactory() - - private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) - private val tangemTechApi: TangemTechApi = mockk() - private val customTokensMerger: CustomTokensMerger = mockk() - private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) - private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() - private val expressServiceFetcher: ExpressServiceFetcher = mockk(relaxUnitFun = true) - - private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( - demoConfig = DemoConfig, - userWalletsListRepository = userWalletsListRepository, - tangemTechApi = tangemTechApi, - customTokensMerger = customTokensMerger, - userTokensResponseStore = userTokensResponseStore, - userTokensSaver = userTokensSaver, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - expressServiceFetcher = expressServiceFetcher, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @BeforeEach - fun resetMocks() { - clearMocks( - userWalletsListRepository, - tangemTechApi, - userTokensResponseStore, - userTokensSaver, - cardCryptoCurrencyFactory, - expressServiceFetcher, - ) - } - - @Test - fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - 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) - - // Assert - val expected = IllegalStateException( - "${DefaultMultiWalletCryptoCurrenciesFetcher::class.simpleName} supports only multi-currency wallet", - ).left() - assertEither(actual, expected) - - verifyOrder { userWalletsListRepository.userWallets } - coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(any()) - } - } - - @Test - fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "AC01000000041225" - } - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null - every { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - } - - @Test - fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "AC01000000041225" - } - - val apiResponse = ApiResponse.Success( - data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), - ) - - 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 { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - } returns apiResponse.data - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if CARD ISN'T DEMO`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - val apiResponse = ApiResponse.Success( - data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), - ) - - 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) - } returns apiResponse.data - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = apiResponse.data.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.TimeoutException(), - ) as ApiResponse - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - 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 { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensSaver.push(userWalletId = any(), response = any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.TimeoutException(), - ) as ApiResponse - - 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 { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - } returns defaultResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensSaver.push(userWalletId = any(), response = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.HttpException( - code = ApiResponseError.HttpException.Code.NOT_FOUND, - message = null, - errorBody = null, - ), - ) as ApiResponse - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - 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 { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - } - - @Test - fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.HttpException( - code = ApiResponseError.HttpException.Code.NOT_FOUND, - message = null, - errorBody = null, - ), - ) as ApiResponse - - 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 { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - } returns defaultResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - private companion object { - val userWalletId = UserWalletId("011") - - val defaultResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - UserTokensResponse.Token( - id = null, - networkId = "bitcoin", - derivationPath = null, - name = "Bitcoin", - symbol = "BTC", - decimals = 8, - contractAddress = null, - addresses = listOf(), - ), - ), - ) - - fun UserTokensResponse.toAssetId(): Set { - return tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - } - } -} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt index 85eb924072..849883a543 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt @@ -13,7 +13,6 @@ import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.models.Eip7702Authorization import com.tangem.domain.transaction.models.GaslessSignedTransactionResult import com.tangem.domain.transaction.models.GaslessTransactionData -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -26,7 +25,6 @@ class DefaultGaslessTransactionRepository( private val gaslessTxServiceApi: GaslessTxServiceApi, private val coroutineDispatcherProvider: CoroutineDispatcherProvider, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, - private val sendFeatureToggles: SendFeatureToggles, ) : GaslessTransactionRepository { private val supportedTokensState = MutableStateFlow>>(hashMapOf()) @@ -131,9 +129,6 @@ class DefaultGaslessTransactionRepository( } override suspend fun getGaslessFeeAddresses(): Set { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return EMPTY_ADDRESSES - } return allAddressesMutex.withLock { allFeeRecipientAddress.ifEmpty { val allFeeAddresses = getAllFeeRecipientAddresses() @@ -150,6 +145,5 @@ class DefaultGaslessTransactionRepository( private companion object { val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("60000") - val EMPTY_ADDRESSES = emptySet() } } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index 8083f49496..2a38ea32b4 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -16,7 +16,6 @@ import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -77,13 +76,11 @@ internal object TransactionDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, gaslessTxServiceApi: GaslessTxServiceApi, coroutineDispatcherProvider: CoroutineDispatcherProvider, - sendFeatureToggles: SendFeatureToggles, ): GaslessTransactionRepository { return DefaultGaslessTransactionRepository( gaslessTxServiceApi = gaslessTxServiceApi, coroutineDispatcherProvider = coroutineDispatcherProvider, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, - sendFeatureToggles = sendFeatureToggles, ) } } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 67768d9958..ef2ebc1613 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -15,6 +15,7 @@ android { dependencies { /** Project - Data */ + implementation(projects.core.analytics) implementation(projects.core.datasource) implementation(projects.core.error) implementation(projects.core.error.ext) @@ -38,11 +39,6 @@ dependencies { implementation(projects.domain.common) implementation(projects.features.swap.domain) - /** Feature API - remove after removing [HotWalletFeatureToggles] */ - implementation(projects.features.hotWallet.api) - - /** Feature API - remove after removing [TangemPayFeatureToggles] */ - implementation(projects.features.tangempay.details.api) /** Project - Utils */ implementation(projects.core.utils) @@ -53,6 +49,7 @@ dependencies { implementation(projects.libs.visa) /** Libs - Other */ + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.arrow.fx) @@ -70,6 +67,6 @@ dependencies { implementation(projects.libs.tangemSdkApi) /** DI */ - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 2d20b15a27..c3eb76035e 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 @@ -104,7 +104,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( map { wallet -> async { val isCustomer = onboardingRepository - .checkCustomerWallet(wallet.walletId) + .hasTangemPayInWallet(wallet.walletId) .getOrNull() == true wallet to isCustomer } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt new file mode 100644 index 0000000000..db6fbcca4f --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { + return when (value) { + is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() + is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) + is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() + is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() + is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() + // Transient statuses are not persisted + is PaymentAccountStatus.Loading, + is PaymentAccountStatus.Error.ExposedDevice, + is PaymentAccountStatus.Error.Unavailable, + is PaymentAccountStatus.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { + return when (value) { + is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed + is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated + is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) + is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) + is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + ) + is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( + source = StatusSource.CACHE, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 5868e4c0c6..565fdbe700 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 @@ -1,13 +1,28 @@ package com.tangem.data.pay.di +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* +import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -16,11 +31,15 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -75,7 +94,51 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager + @Binds + @Singleton + fun bindPaymentAccountStatusProducerFactory( + impl: DefaultPaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusProducer.Factory + + @Binds + @Singleton + fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher + companion object { + + @Provides + @Singleton + fun providePaymentAccountStatusesStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): PaymentAccountStatusesStore { + return PaymentAccountStatusesStore( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun providePaymentAccountStatusSupplier( + factory: PaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusSupplier { + return object : PaymentAccountStatusSupplier( + factory = factory, + keyCreator = { "payment_account_status_${it.userWalletId.stringValue}" }, + ) {} + } + @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..cdfbf0d9b4 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -0,0 +1,178 @@ +package com.tangem.data.pay.flow + +import arrow.core.Either +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import timber.log.Timber +import javax.inject.Inject + +private const val TAG = "PaymentAccountStatusFetcher" + +internal class DefaultPaymentAccountStatusFetcher @Inject constructor( + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val onboardingRepository: OnboardingRepository, + private val customerOrderRepository: CustomerOrderRepository, + private val deviceSecurity: DeviceSecurityInfoProvider, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusFetcher { + + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = + eitherOn(dispatchers.default) { + Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}") + + if (deviceSecurity.isSecurityExposed()) { + Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + + return@eitherOn paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = PaymentAccountStatus.Error.ExposedDevice, + ) + } + + val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) + .fold( + ifLeft = { error -> + Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + when (error) { + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ifRight = { hasTangemPay -> + proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + }, + ) + Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + } + + private suspend fun proceedHasTangemPayResult( + userWalletId: UserWalletId, + hasTangemPay: Boolean, + ): PaymentAccountStatus { + Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + return if (hasTangemPay) { + fetchTangemPayAccountStatus(userWalletId = userWalletId) + } else { + PaymentAccountStatus.NotCreated + } + } + + private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) + if (prevResult == null || prevResult is PaymentAccountStatus.Error) { + paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + } + + return proceedWithOrderId(userWalletId = userWalletId) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { + return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + PaymentAccountStatus.Error.NotSynced + } else { + val orderId = onboardingRepository.getOrderId(userWalletId) + if (orderId != null) { + proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + } else { + proceedWithoutOrder(userWalletId = userWalletId) + } + } + } + + private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { + return onboardingRepository.getCustomerInfo(userWalletId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { customerInfo -> + Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + val status = customerInfo.mapToPaymentAccountStatus() + if (status is PaymentAccountStatus.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(userWalletId) + } + status + }, + ) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { orderData -> + Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + when (orderData.status) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + + OrderStatus.CANCELED -> { + // If order was cancelled -> clear previous order from local storage and start order creation + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.createOrder(userWalletId) + PaymentAccountStatus.Error.CardIssueFailed + } + OrderStatus.COMPLETED -> { + // Order was completed -> clear order id and get customer info + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + .fold( + ifLeft = { it.mapToPaymentAccountStatus() }, + ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ) + } + OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ) + } + + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + val cardInfo = this.cardInfo + val productInstance = this.productInstance + return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { + PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) + } else if (cardInfo != null && productInstance != null) { + PaymentAccountStatus.Loaded( + source = StatusSource.ACTUAL, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + balance = cardInfo.balance, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + ) + } else { + PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + return when (this) { + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..cb021c1ee8 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -0,0 +1,37 @@ +package com.tangem.data.pay.flow + +import arrow.core.Option +import arrow.core.some +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( + @Assisted private val params: PaymentAccountStatusProducer.Params, + override val flowProducerTools: FlowProducerTools, + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusProducer { + override val fallback: Option + get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() + + override fun produce(): Flow { + return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) + .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : PaymentAccountStatusProducer.Factory { + override fun create(params: PaymentAccountStatusProducer.Params): DefaultPaymentAccountStatusProducer + } +} \ 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 f479794766..508627ca37 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,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.OrderResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus @@ -19,11 +20,11 @@ internal class DefaultCustomerOrderRepository @Inject constructor( tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> 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 + null -> OrderStatus.PROCESSING + OrderResponse.Result.Status.NEW -> OrderStatus.NEW + OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING + OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED + OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED } OrderData( status = status, 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 2fdcfabcb6..b67a28e4e5 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 @@ -2,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.raise.catch +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest @@ -10,6 +11,7 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.datasource.TangemPayAuthDataSource @@ -17,6 +19,7 @@ import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -27,13 +30,11 @@ import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject private const val VALID_STATUS = "valid" -private const val APPROVED_KYC_STATUS = "approved" -private const val IN_PROGRESS_KYC_STATUS = "in_progress" -private const val DECLINED_KYC_STATUS = "declined" private const val TAG = "TangemPay: OnboardingRepository" @Suppress("LongParameterList") internal class DefaultOnboardingRepository @Inject constructor( + private val analytics: AnalyticsEventHandler, private val dispatcherProvider: CoroutineDispatcherProvider, private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, @@ -137,6 +138,9 @@ internal class DefaultOnboardingRepository @Inject constructor( userWalletId: UserWalletId, response: CustomerMeResponse.Result?, ): CustomerInfo { + val kycStatus = KycStatus.fromString(status = response?.kyc?.status) + sendKycAnalytics(kycStatus) + val card = response?.card val fiatBalance = response?.balance?.fiat val paymentAccount = response?.paymentAccount @@ -145,7 +149,6 @@ internal class DefaultOnboardingRepository @Inject constructor( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, - customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, ) @@ -159,19 +162,30 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState) + ProductInstance(id = instance.id, cardId = instance.cardId) } return CustomerInfo( customerId = response?.id, productInstance = productInstance, - kycStatus = getKycStatus(status = response?.kyc?.status), + kycStatus = kycStatus, cardInfo = cardInfo, ).also { lastFetchedCustomerInfoMap[userWalletId] = it } } - override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either { + private fun sendKycAnalytics(kycStatus: KycStatus) { + val event = when (kycStatus) { + KycStatus.APPROVED -> TangemPayAnalyticsEvents.KycPassedAndOrderCreated() + KycStatus.REJECTED -> TangemPayAnalyticsEvents.KycRejected() + KycStatus.INIT, + KycStatus.PENDING, + -> return + } + analytics.send(event) + } + + override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either { val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) if (hasTangemPay != null) { return Either.Right(hasTangemPay) @@ -228,13 +242,4 @@ internal class DefaultOnboardingRepository @Inject constructor( setHideMainOnboardingBanner(userWalletId) } } - - private fun getKycStatus(status: String?): CustomerInfo.KycStatus { - return when (status?.lowercase()) { - IN_PROGRESS_KYC_STATUS -> CustomerInfo.KycStatus.PENDING - DECLINED_KYC_STATUS -> CustomerInfo.KycStatus.REJECTED - APPROVED_KYC_STATUS -> CustomerInfo.KycStatus.APPROVED - else -> CustomerInfo.KycStatus.INIT - } - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 3252c49c4e..61f222a421 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -16,10 +16,10 @@ import com.tangem.datasource.api.pay.models.request.CardDetailsRequest import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest import com.tangem.datasource.api.pay.models.request.SetPinRequest import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse +import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails @@ -279,15 +279,15 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( orderStatus.onRight { response -> val status = response.result?.status - if (status == OrderStatus.COMPLETED.apiName || status == OrderStatus.CANCELED.apiName) { + if (status == Status.COMPLETED || status == Status.CANCELED) { // Remove from jobs pollingJobs.remove(key = orderId) // Final card state val finalState = when { - status == OrderStatus.COMPLETED.apiName && isFreeze + status == Status.COMPLETED && isFreeze -> TangemPayCardFrozenState.Frozen - status == OrderStatus.COMPLETED.apiName && !isFreeze + status == Status.COMPLETED && !isFreeze -> TangemPayCardFrozenState.Unfrozen else -> return@launch } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index 021605d89a..eab58caa05 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -77,34 +77,22 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( limit: Int, ): List { cacheRegistry.invokeOnExpire( - key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor), + key = getCacheKey(userWalletId = userWalletId, cursor = cursor), skipCache = config.shouldRefresh, - block = { - fetch( - userWalletId = userWalletId, - customerWalletAddress = config.customerWalletAddress, - cursor = cursor, - pageSize = limit, - ) - }, + block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) }, ) return txHistoryItemsStore.getSyncOrNull( - key = config.customerWalletAddress, + key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, ).orEmpty() } - private fun getCacheKey(customerWalletAddress: String, cursor: String?): String { - return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}" + private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String { + return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}" } - private suspend fun fetch( - userWalletId: UserWalletId, - customerWalletAddress: String, - cursor: String?, - pageSize: Int, - ) { + private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) { requestPerformer.performRequest(userWalletId = userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.onLeft { @@ -112,7 +100,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( }.onRight { response -> val result = response.result val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() - txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) + txHistoryItemsStore.store(key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, value = items) } } } \ 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 index 7bf21432a7..08686ded72 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -23,7 +23,13 @@ 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.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -36,6 +42,8 @@ import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds private const val TAG = "TangemPaySwapRepository" +private const val MAX_POLLING_ATTEMPTS = 6 +private data class PollingKey(val userWalletId: String, val orderId: String) @Suppress("LongParameterList") internal class DefaultTangemPayWithdrawRepository @Inject constructor( @@ -48,9 +56,9 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private val orderRepository: CustomerOrderRepository, ) : TangemPayWithdrawRepository { - private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val withdrawPollingJobs = mutableMapOf() - private val withdrawPollingMutex = Mutex() + private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val pollingJobs = mutableMapOf() + private val pollingMutex = Mutex() override suspend fun withdraw( userWallet: UserWallet, @@ -121,6 +129,76 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + startPollingForOrder(userWallet, orderId, exchangeData) + } + } + } + + private fun startPollingForOrder( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + pollingScope.launch { + startWithdrawOrderPolling(userWallet, orderId, exchangeData) + } + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val key = PollingKey(userWallet.walletId.stringValue, orderId) + + pollingMutex.withLock { + if (pollingJobs.containsKey(key)) return@withLock + + val pollingJob = pollingScope.launch { + try { + var attemptCount = 0 + while (isActive && attemptCount < MAX_POLLING_ATTEMPTS) { + delay(duration = 3.seconds) + attemptCount++ + val result = orderRepository.getOrderData( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + val txHash = result.getOrNull()?.withdrawTxHash?.ifEmpty { null } + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + txHash = txHash, + exchangeData = exchangeData, + orderId = orderId, + ) + pollingMutex.withLock { pollingJobs.remove(key) } + return@launch + } + } + if (attemptCount >= MAX_POLLING_ATTEMPTS) { + Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } + pollingJobs[key] = pollingJob + } + } + + private fun stopPolling(userWalletId: String, orderId: String) { + pollingScope.launch { + pollingMutex.withLock { + val key = PollingKey(userWalletId, orderId) + pollingJobs[key]?.cancel() + pollingJobs.remove(key) } } } @@ -140,7 +218,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( txHash = txHash, payInExtraId = exchangeData.payInExtraId, ).also { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + stopPolling(userWalletId = userWallet.walletId.stringValue, orderId = orderId) } } @@ -157,14 +236,12 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( 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) - } + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) } } } @@ -189,51 +266,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( 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 + startPollingForOrder(userWallet, orderId, exchangeData) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt new file mode 100644 index 0000000000..5b8866a09a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -0,0 +1,86 @@ +package com.tangem.data.pay.store + +import androidx.datastore.core.DataStore +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import timber.log.Timber + +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map + +/** + * Store for payment account statuses with dual storage (runtime + persistence). + * + * @property runtimeStore runtime store for fast in-memory access + * @property persistenceDataStore persistence store for caching across app restarts + */ +internal class PaymentAccountStatusesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + try { + val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + runtimeStore.store( + value = cachedStatuses.mapValues { (_, statusDM) -> + PaymentAccountStatusDMConverter.convertBack(statusDM) + }, + ) + } catch (e: Exception) { + Timber.e(e, "Error while loading cached payment account statuses") + } + } + } + + fun get(userWalletId: UserWalletId): Flow { + return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + } + + suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) + } + + suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status) } + } + } + + suspend fun contains(userWalletId: UserWalletId): Boolean { + return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + put(key = userWalletId.stringValue, value = status) + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { + val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + put(key = userWalletId.stringValue, value = statusDM) + } + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt index 23187867a8..be1d69448d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isPositive import com.tangem.utils.extensions.isZero +import com.tangem.utils.extensions.orZero import org.joda.time.DateTime import org.joda.time.DateTimeZone import timber.log.Timber @@ -37,6 +38,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : date = spend.authorizedAt.withLocalZone(), amount = spend.amount, currency = Currency.getInstance(spend.currency), + authorizedAmount = spend.authorizedAmount.orZero(), localAmount = spend.localAmount, localCurrency = spend.localCurrency?.let(Currency::getInstance), enrichedMerchantName = spend.enrichedMerchantName, 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 65749b77cb..b0012d93a3 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 @@ -20,12 +20,10 @@ 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.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 import com.tangem.domain.walletconnect.WcRequestUseCaseFactory @@ -95,7 +93,6 @@ internal object WalletConnectDataModule { getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, analytics: AnalyticsEventHandler, - accountsFeatureToggles: AccountsFeatureToggles, wcScope: WcScope, ): DefaultWcSessionsManager { return DefaultWcSessionsManager( @@ -104,7 +101,6 @@ internal object WalletConnectDataModule { getWallets = getWallets, wcNetworksConverter = wcNetworksConverter, analytics = analytics, - accountsFeatureToggles = accountsFeatureToggles, scope = wcScope, ) } @@ -184,14 +180,12 @@ internal object WalletConnectDataModule { fun wcNetworksConverter( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, singleAccountSupplier: SingleAccountSupplier, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, singleAccountStatusListSupplier = singleAccountStatusListSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, singleAccountSupplier = singleAccountSupplier, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index d835bc5139..22da45b6f4 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -131,12 +131,9 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( val caip2 = hexChainIdToCAIP2(hexChainId) ?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left() val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet) - if (generalNetwork == null) { - return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() - } + ?: return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest( rawChainId = caip2.raw, - wallet = wallet, account = context.session.account, ) if (addedNetwork == null) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index c03d165fa0..a1d65ed85f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -47,7 +47,6 @@ internal class WcEthNetwork( ?: return error("Failed to parse $name") suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest( rawChainId = chainId, - wallet = wallet, account = account, ) @@ -74,7 +73,7 @@ internal class WcEthNetwork( -> anyExistNetwork() } ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") - val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index bb68bd46d2..0265091789 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -49,7 +49,7 @@ internal class WcSolanaNetwork( val wallet = session.wallet val account = session.account val chainId = request.chainId.orEmpty() - suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet, account) + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, account) suspend fun anyAddress() = anyExistNetwork() ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() @@ -64,7 +64,7 @@ internal class WcSolanaNetwork( ?: anyExistNetwork() ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") - val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, @@ -86,7 +86,7 @@ internal class WcSolanaNetwork( override val namespaceKey: NamespaceKey = NamespaceKey("solana") override fun toBlockchain(chainId: CAIP2): Blockchain? { - val isMainNet = MAINNET_CHAIN_ID.any { it.lowercase() == chainId.reference.lowercase() } + val isMainNet = MAINNET_CHAIN_ID.any { it.equals(chainId.reference, ignoreCase = true) } if (chainId.namespace != namespaceKey.key) return null return when { isMainNet -> Blockchain.Solana diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index a1384944b8..c35cd5c848 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -8,7 +8,6 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.model.WcPairError @@ -23,19 +22,6 @@ internal class AssociateNetworksDelegate( private val getWallets: GetWalletsUseCase, ) { - @Throws(WcPairError.UnsupportedBlockchains::class) - suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { - val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } - val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() - val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() - // remove duplicates - .subtract(requiredNamespaces) - - return userWallets.associateWith { wallet -> - mapNetworksForPortfolio(wallet, null, requiredNamespaces, optionalNamespaces, sessionProposal) - } - } - @Throws(WcPairError.UnsupportedBlockchains::class) suspend fun associateAccounts(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync() @@ -67,13 +53,12 @@ internal class AssociateNetworksDelegate( @Suppress("CyclomaticComplexMethod") private suspend fun mapNetworksForPortfolio( wallet: UserWallet, - account: Account?, + account: Account, requiredNamespaces: Set, optionalNamespaces: Set, sessionProposal: Wallet.Model.SessionProposal, ): ProposalNetwork { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(userWalletId = wallet.walletId) + val portfolioNetworks = getAccountNetworks(account.accountId) val unknownRequired = mutableSetOf() val unknownOptional = mutableSetOf() @@ -127,12 +112,6 @@ internal class AssociateNetworksDelegate( ) } - private suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return networksConverter.getWalletNetworks(userWalletId) - // flatten all derivation - .distinctBy { it.rawId } - } - private suspend fun getAccountNetworks(accountId: AccountId): List { return networksConverter.getAccountNetworks(accountId) // flatten all derivation diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index f07bf2bd9d..bf81785e59 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -10,7 +10,6 @@ import com.reown.walletkit.client.Wallet import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.getDappOriginUrl -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* @@ -35,7 +34,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( private val sdkDelegate: WcPairSdkDelegate, private val blockAidVerifier: BlockAidVerifier, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, @Assisted private val pairRequest: WcPairRequest, ) : WcPairUseCase { @@ -113,7 +111,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val sessionDTO = WcSessionDTO( topic = "", walletId = sessionForApprove.wallet.walletId, - accountId = sessionForApprove.account?.accountId, + accountId = sessionForApprove.account.accountId, url = sdkVerifyContext.getDappOriginUrl(), securityStatus = proposalState.dAppSession.securityStatus, connectingTime = connectingTime, @@ -128,7 +126,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( pendingSessionForSave = pendingSessionForSave, sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, - ).map { settledSession -> + ).map { _ -> analytics.send( WcAnalyticEvents.DAppConnected( sessionProposal = proposalState.dAppSession, @@ -197,12 +195,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, ): Either = runCatching { - val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) - val proposalAccountNetwork = if (accountsFeatureToggles.isFeatureEnabled) { - associateNetworksDelegate.associateAccounts(sessionProposal) - } else { - null - } + val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal) val verificationInfo = when { verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE verifyContext.isScam == true -> CheckDAppResult.UNSAFE @@ -211,7 +204,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( CheckDAppResult.FAILED_TO_VERIFY } } - val requestedNetworks = (proposalAccountNetwork ?: proposalNetwork) + val requestedNetworks = proposalAccountNetwork .values.map { it.available.plus(it.required) }.flatten().toSet() analytics.send( WcAnalyticEvents.PairRequested( @@ -230,7 +223,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) val dAppSession = WcSessionProposal( dAppMetaData = appMetaData, - proposalNetwork = proposalNetwork, securityStatus = verificationInfo, proposalAccountNetwork = proposalAccountNetwork, ) 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 8f37e4e4a1..96a3982807 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 @@ -8,9 +8,7 @@ import com.reown.walletkit.client.WalletKit 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 import com.tangem.domain.walletconnect.model.WcSession @@ -34,20 +32,16 @@ internal class DefaultWcSessionsManager( private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val scope: WcScope, ) : WcSessionsManager, WcSdkObserver { private val onSessionDelete = Channel(capacity = Channel.BUFFERED) - private val oneTimeMigration = MutableStateFlow(false) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } .transform { pair -> val (wallets, inStore) = pair val inSdk: List = WalletKit.getListOfActiveSessions() - val someMigrate = migrateToAccountSession(inStore) - if (someMigrate) return@transform val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one @@ -56,26 +50,6 @@ internal class DefaultWcSessionsManager( .distinctUntilChanged() .flowOn(dispatchers.io) - private suspend fun migrateToAccountSession(inStore: Set): Boolean { - if (!accountsFeatureToggles.isFeatureEnabled) return false - if (oneTimeMigration.value) return false - - var someMigrated = false - - val updatedSessions = inStore.mapTo(mutableSetOf()) { sessionDTO -> - if (sessionDTO.accountId == null) { - someMigrated = true - val mainAccountId = AccountId.forMainCryptoPortfolio(sessionDTO.walletId) - sessionDTO.copy(accountId = mainAccountId) - } else { - sessionDTO - } - } - if (someMigrated) store.saveSessions(updatedSessions) - oneTimeMigration.value = true - return someMigrated - } - override fun onWcSdkInit() { listenOnSessionDelete() extendSessions() @@ -117,10 +91,8 @@ 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) } as? Account.CryptoPortfolio - if (accountsFeatureToggles.isFeatureEnabled && account == null) { - return@mapNotNull null - } + val account = wcNetworksConverter.getAccount(storeSession.accountId) as? Account.CryptoPortfolio + ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession) val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: "" WcSession( 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 ebf56dafcf..d3c3d64bdf 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 @@ -71,7 +71,7 @@ internal class WcSignUseCaseDelegate( network = context.network, errorCode = error.code(), errorMessage = errorMessage, - accountDerivation = context.session.account?.derivationIndex?.value, + accountDerivation = context.session.account.derivationIndex.value, ) analytics.send(event) } 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 d4b19d974a..9104712d62 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 @@ -18,8 +18,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -31,7 +29,6 @@ internal class WcNetworksConverter @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val singleAccountSupplier: SingleAccountSupplier, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) { fun createNetwork(chainId: String, wallet: UserWallet): Network? { @@ -47,13 +44,12 @@ internal class WcNetworksConverter @Inject constructor( val wallet = session.wallet val allCoinNetwork = filterWalletNetworkForRequest( rawChainId = request.chainId.orEmpty(), - wallet = session.wallet, account = session.account, ) val requestNetwork = allCoinNetwork.find { network -> val address = getAddressForWC(wallet.walletId, network) - requestAddress.lowercase() == address?.lowercase() + requestAddress.equals(address, ignoreCase = true) } return requestNetwork } @@ -61,13 +57,13 @@ internal class WcNetworksConverter @Inject constructor( /** * return network with not custom derivationPath or first custom or any */ - suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet, account: Account?): Network? { - val networks = filterWalletNetworkForRequest(rawChainId, wallet, account) + suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, account: Account): Network? { + val networks = filterWalletNetworkForRequest(rawChainId, account) return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() } - suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account?): List { - return filterWalletNetworkForRequest(rawChainId, wallet, account) + suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account): List { + return filterWalletNetworkForRequest(rawChainId, account) .mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() } } @@ -85,13 +81,8 @@ internal class WcNetworksConverter @Inject constructor( /** * return all exist derivation networks */ - suspend fun filterWalletNetworkForRequest( - rawChainId: String, - wallet: UserWallet, - account: Account?, - ): List { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(wallet.walletId) + suspend fun filterWalletNetworkForRequest(rawChainId: String, account: Account): List { + val portfolioNetworks = getAccountNetworks(account.accountId) val blockchain = namespaceConverters .firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf() @@ -102,11 +93,10 @@ internal class WcNetworksConverter @Inject constructor( suspend fun findWalletNetworks( wallet: UserWallet, - account: Account?, + account: Account, sdkSession: Wallet.Model.Session, ): Set { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(wallet.walletId) + val portfolioNetworks = getAccountNetworks(account.accountId) val existNetworks = sdkSession.namespaces.values .map { it.accounts }.flatten().toSet() .mapNotNull { CAIP10.fromRaw(it) } @@ -120,7 +110,7 @@ internal class WcNetworksConverter @Inject constructor( // find equal address .firstOrNull { network -> val walletAddress = getAddressForWC(wallet.walletId, network) - walletAddress?.lowercase() == caip10.accountAddress.lowercase() + walletAddress.equals(caip10.accountAddress, ignoreCase = true) } } @@ -132,21 +122,12 @@ internal class WcNetworksConverter @Inject constructor( } suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { - val portfolioNetworks = sessionForApprove.account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(sessionForApprove.wallet.walletId) + val portfolioNetworks = getAccountNetworks(sessionForApprove.account.accountId) return sessionForApprove.network .map { network -> portfolioNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } } .flatten() } - suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ) - .orEmpty() - .filterIsInstance().map(CryptoCurrency.Coin::network) - } - private suspend fun getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio? { return singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(accountId.userWalletId), diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 6313e3ddf4..afaed251c9 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -14,8 +14,8 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.utils.WcSdkSessionConverter -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest @@ -37,7 +37,6 @@ internal class DefaultWcPairUseCaseTest { private val analytics: AnalyticsEventHandler = mockk(relaxed = true) private val sdkDelegate: WcPairSdkDelegate = mockk() private val blockAidVerifier: BlockAidVerifier = mockk() - private val accountsFeatureToggles = mockk() private val url = "testUrl" private val source = WcPairRequest.Source.QR @@ -75,7 +74,7 @@ internal class DefaultWcPairUseCaseTest { get() = WcSessionApprove( wallet = MockUserWalletFactory.create(), network = listOf(), - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), ) private val sdkApprove: Wallet.Params.SessionApprove @@ -108,7 +107,7 @@ internal class DefaultWcPairUseCaseTest { networks = setOf(), connectingTime = null, showWalletInfo = false, - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), ) private fun useCaseFactory() = DefaultWcPairUseCase( @@ -117,14 +116,12 @@ internal class DefaultWcPairUseCaseTest { sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, analytics = analytics, - accountsFeatureToggles = accountsFeatureToggles, pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source), ) @Before fun setup() { - coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf() - coEvery { accountsFeatureToggles.isFeatureEnabled } returns false + coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf() coEvery { caipNamespaceDelegate.associate( sessionProposal = sdkProposal, @@ -257,7 +254,6 @@ internal class DefaultWcPairUseCaseTest { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) - associateNetworksDelegate.associate(sdkProposal) blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin)) } assert(awaitItem() is WcPairState.Proposal) diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index d703676123..069d84224c 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -10,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.* import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.models.account.Account import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData @@ -58,7 +59,7 @@ internal class WcSignUseCaseDelegateTest { session = WcSession( wallet = MockUserWalletFactory.create(), networks = setOf(), - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), securityStatus = CheckDAppResult.FAILED_TO_VERIFY, connectingTime = 0L, sdkModel = WcSdkSession( 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 83af408c47..8d90aedc75 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 @@ -5,17 +5,14 @@ import arrow.core.left import arrow.core.right import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -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.preferences.PreferencesKeys @@ -24,7 +21,6 @@ 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 @@ -52,10 +48,7 @@ internal class DefaultWalletsRepository( private val userWalletsListRepository: UserWalletsListRepository, private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, - private val authProvider: AuthProvider, private val walletServerBinder: WalletServerBinder, - private val appsFlyerStore: AppsFlyerStore, - private val accountsFeatureToggles: AccountsFeatureToggles, private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { @@ -368,59 +361,36 @@ internal class DefaultWalletsRepository( override suspend fun associateWallets(applicationId: String, wallets: List) = withContext(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled) { - val associateApplicationIdWithWallets: suspend () -> ApiResponse = { - tangemTechApi.associateApplicationIdWithWalletsV2( - applicationId = applicationId, - body = AssociateApplicationIdWithWalletsBody( - walletIds = wallets.map { it.walletId.stringValue }.distinct(), - ), - ) - } - - val apiResponse = associateApplicationIdWithWallets() - - if (apiResponse is ApiResponse.Success) return@withContext - - if (apiResponse is ApiResponse.Error && - apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) - ) { - val errorBody = (apiResponse.cause as? HttpException)?.errorBody - ?: error("Bad Request must have error body") - - val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) - val errorResponse = adapter.fromJson(errorBody) - ?: error("Cannot parse error body: $errorBody") - - errorResponse.missingWalletIds - .map { - async { createWallet(userWalletId = UserWalletId(it)) } - } - .awaitAll() - - associateApplicationIdWithWallets().getOrThrow() - } - } else { - val conversionData = appsFlyerStore.get() - val publicKeys = authProvider.getCardsPublicKeys() - val walletsBody = wallets.map { userWallet -> - WalletIdBodyConverter.convert( - userWallet = userWallet, - conversionData = conversionData, - publicKeys = if (userWallet is UserWallet.Cold) { - publicKeys.filterKeys { - userWallet.cardsInWallet.contains(it) - } - } else { - emptyMap() - }, - ) - } - - tangemTechApi.associateApplicationIdWithWallets( + val associateApplicationIdWithWallets: suspend () -> ApiResponse = { + tangemTechApi.associateApplicationIdWithWalletsV2( applicationId = applicationId, - body = walletsBody, - ).getOrThrow() + body = AssociateApplicationIdWithWalletsBody( + walletIds = wallets.map { it.walletId.stringValue }.distinct(), + ), + ) + } + + val apiResponse = associateApplicationIdWithWallets() + + if (apiResponse is ApiResponse.Success) return@withContext + + if (apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) + ) { + val errorBody = (apiResponse.cause as? HttpException)?.errorBody + ?: error("Bad Request must have error body") + + val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) + val errorResponse = adapter.fromJson(errorBody) + ?: error("Cannot parse error body: $errorBody") + + errorResponse.missingWalletIds + .map { + async { createWallet(userWalletId = UserWalletId(it)) } + } + .awaitAll() + + associateApplicationIdWithWallets().getOrThrow() } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt new file mode 100644 index 0000000000..56cfcb4a13 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt @@ -0,0 +1,101 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation +import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.ColdCurvesConfig +import com.tangem.domain.wallets.config.CurvesConfig +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** + * Source of derivations data + */ +internal sealed interface DerivationsSource { + + val isHDWalletAllowed: Boolean + val hasOldStyleDerivation: Boolean + val curvesConfig: CurvesConfig + val derivationStyleProvider: DerivationStyleProvider + + fun getWalletPublicKey(curve: EllipticCurve): ByteArray? + fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap + + data class FromUserWallet(val userWallet: UserWallet) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.settings.isHDWalletAllowed + is UserWallet.Hot -> true + } + + override val hasOldStyleDerivation: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.hasOldStyleDerivation + is UserWallet.Hot -> false + } + + override val curvesConfig: CurvesConfig + get() = userWallet.curvesConfig + + override val derivationStyleProvider: DerivationStyleProvider + get() = userWallet.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getWalletPublicKey(curve) + is UserWallet.Hot -> userWallet.wallets + ?.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey + } + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getDerivedKeys(publicKey) + is UserWallet.Hot -> { + val derivedKeys = userWallet.wallets + ?.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) } + ?.derivedKeys + .orEmpty() + + ExtendedPublicKeysMap(derivedKeys) + } + } + } + } + + data class FromScanResponse(val scanResponse: ScanResponse) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = scanResponse.card.settings.isHDWalletAllowed + + override val hasOldStyleDerivation: Boolean + get() = scanResponse.card.hasOldStyleDerivation + + override val curvesConfig: CurvesConfig + get() = ColdCurvesConfig(scanResponse.card) + + override val derivationStyleProvider: DerivationStyleProvider + get() = scanResponse.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return scanResponse.getWalletPublicKey(curve) + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return scanResponse.getDerivedKeys(publicKey) + } + } +} + +private fun ScanResponse.getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return card.wallets.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey +} + +private fun ScanResponse.getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index ce12ab7516..3ea84acd9e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -3,45 +3,80 @@ package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.config.curvesConfig -import com.tangem.domain.wallets.derivations.derivationStyleProvider -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import kotlin.collections.forEach private typealias DerivationData = Pair> internal typealias Derivations = Map> +/** + * Data class representing a blockchain with its derivation path + */ +data class BlockchainToDerive( + val blockchain: Blockchain, + val derivationPath: DerivationPath, +) + /** * Finder of missed derivations * - * @property userWallet User wallet to find derivations for + * @property source Source of derivations data (UserWallet or ScanResponse) * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val userWallet: UserWallet) { +class MissedDerivationsFinder private constructor(private val source: DerivationsSource) { + + /** + * Secondary constructor for backward compatibility with UserWallet + */ + constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet)) + + /** + * Secondary constructor for ScanResponse + */ + constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse)) /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { return currencies.map { it.network }.let(::findByNetworks) } + /** Find missed derivations for given [Network] list */ fun findByNetworks(networks: List): Derivations { + val blockchainsToDerive = networks.mapNotNull { network -> + val blockchain = network.toBlockchain() + val derivationPath = network.derivationPath.value?.let(::DerivationPath) + ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + return findByBlockchainsToDerive(blockchainsToDerive) + } + + /** Find missed derivations for given [BlockchainToDerive] list */ + fun findByBlockchainsToDerive(blockchainsToDerive: Collection): Derivations { + val enrichedBlockchains = blockchainsToDerive.enrichBlockchains() + return findDerivationsInternal(enrichedBlockchains) + } + + /** + * Common implementation for finding derivations + */ + private fun findDerivationsInternal(items: Collection): Derivations { return buildMap> { - networks - .mapToNewDerivations() + items + .mapNotNull(::mapToNewDerivation) .forEach { data -> val current = this[data.first] if (current != null) { current.addAll(data.second) - current.distinct() + this[data.first] = current.distinct().toMutableList() } else { this[data.first] = data.second.toMutableList() } @@ -49,31 +84,17 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } } - private fun List.mapToNewDerivations(): List { - return mapNotNull { network -> - val blockchain = network.toBlockchain() - val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null + /** + * Maps a single BlockchainToDerive to derivation data (public key -> derivation paths) + */ + private fun mapToNewDerivation(input: BlockchainToDerive): DerivationData? { + val curve = source.curvesConfig.primaryCurve(input.blockchain) ?: return null + if (!input.blockchain.getSupportedCurves().contains(curve)) return null - val walletPublicKey = when (userWallet) { - is UserWallet.Cold -> { - val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } - wallet?.publicKey - } - is UserWallet.Hot -> { - val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } - wallet?.publicKey - } - } + val publicKey = source.getWalletPublicKey(curve) ?: return null - walletPublicKey?.let { - findNewDerivations(curve = curve, publicKey = it, network = network) - } - } - } - - private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { - val derivationCandidates = network - .getDerivationCandidates(curve) + val derivationCandidates = input.blockchain + .getDerivationCandidates(input.derivationPath) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } @@ -81,59 +102,63 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { return publicKey.toMapKey() to derivationCandidates } - private fun Network.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = this.toBlockchain() - + /** + * Gets all possible derivation paths for a blockchain + */ + private fun Blockchain.getDerivationCandidates(derivationPath: DerivationPath): List { return buildList { - add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) + // Default derivation path for blockchain + add(getDerivationPath()) + + // The specified derivation path (can be either default or custom) + add(derivationPath) + + // Extended Cardano derivation path if needed + add(getCardanoExtendedDerivationPath(derivationPath)) } .filterNotNull() .distinct() } - private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) - } else { - null - } + private fun Blockchain.getDerivationPath(): DerivationPath? { + return derivationPath(style = source.derivationStyleProvider.getDerivationStyle()) } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - network.derivationPath.value?.let(::DerivationPath) - } else { - null - } - } - - private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { - return if (this == Blockchain.Cardano) { - network.derivationPath.value?.let { - CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) - } - } else { - null - } + private fun Blockchain.getCardanoExtendedDerivationPath(customDerivationPath: DerivationPath): DerivationPath? { + if (this != Blockchain.Cardano) return null + return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath) } private fun List.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey) + val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList() return filterNot(alreadyDerivedPaths::contains) } - private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = when (userWallet) { - is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) - is UserWallet.Hot -> { - val wallets = userWallet.wallets ?: return emptyList() - wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys - ?: ExtendedPublicKeysMap(emptyMap()) - } + // region Blockchain enrichment logic + + /** + * Enriches blockchains collection: + * - Adds Ethereum if HD wallet is allowed + * - Removes unnecessary blockchains that share derivation path with Ethereum (for cards without old style derivation) + */ + private fun Collection.enrichBlockchains(): Collection { + if (!source.isHDWalletAllowed) return this + + val derivationStyle = source.derivationStyleProvider.getDerivationStyle() + val ethereumDerivationPath = Blockchain.Ethereum.derivationPath(derivationStyle) ?: return this + + val withEthereum = this + BlockchainToDerive(Blockchain.Ethereum, ethereumDerivationPath) + + // For cards with old style derivation, keep all blockchains + if (source.hasOldStyleDerivation) { + return withEthereum.distinct() } - return extendedPublicKeysMap.keys.toList() + // For new cards: filter out blockchains with same derivation path as Ethereum (except Ethereum itself) + return withEthereum + .filter { it.derivationPath != ethereumDerivationPath || it.blockchain == Blockchain.Ethereum } + .distinct() } + + // endregion } \ 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 c580d60ae1..c7561aab8b 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 @@ -9,13 +9,11 @@ import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.derivations.DefaultDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -43,10 +41,7 @@ internal object WalletsDataModule { tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, - authProvider: AuthProvider, walletServerBinder: WalletServerBinder, - appsFlyerStore: AppsFlyerStore, - accountsFeatureToggles: AccountsFeatureToggles, @NetworkMoshi moshi: Moshi, ): WalletsRepository { return DefaultWalletsRepository( @@ -55,10 +50,7 @@ internal object WalletsDataModule { userWalletsListRepository = userWalletsListRepository, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, - authProvider = authProvider, walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, moshi = moshi, ) } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt index 8a77f2f70a..d3887a70c3 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -47,18 +47,11 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( hotWalletId = hotWalletId, auth = true, ) - val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( - hotWalletId = hotWalletId, - auth = false, - ) - appPreferencesStore.editData { - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + appPreferencesStore.editData { data -> + data.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) } } @@ -119,13 +112,21 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( } else -> { val remaining = remainingSeconds(deadlineElapsed, bootStored) - Attempts.WithDelay(count, remaining) + val newCount = if (id.auth) { + count + } else { + MAX_FAST_FORWARD_ATTEMPTS + } + Attempts.WithDelay(newCount, remaining) } } } private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { - return "${hotWalletId.value}_$auth" + // Regarding [REDACTED_TASK_KEY], the attempts counter must be shared between modes (auth vs signing). + // To provide backward compatibility, we use the same keys but read attempts in auth mode for security reasons. + val isAuthMode = true + return "${hotWalletId.value}_$isAuthMode" } private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) 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 71182a19c0..74d60d237c 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 @@ -5,149 +5,149 @@ import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import com.tangem.data.common.wallet.WalletServerBinder -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +/** + * Tests for [DefaultWalletsRepository] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultWalletsRepositoryTest { - private lateinit var repository: DefaultWalletsRepository - private val preferencesDataStore = mockk>(relaxed = true) + + private val preferencesDataStore: DataStore = mockk(relaxed = true) + private val tangemTechApi: TangemTechApi = mockk() + private val walletServerBinder: WalletServerBinder = mockk() + private val appPreferenceStore = AppPreferencesStore( moshi = Moshi.Builder().build(), dispatchers = TestingCoroutineDispatcherProvider(), preferencesDataStore = preferencesDataStore, ) - private lateinit var tangemTechApi: TangemTechApi - private lateinit var dispatchers: CoroutineDispatcherProvider - private lateinit var walletServerBinder: WalletServerBinder - private lateinit var appsFlyerStore: AppsFlyerStore + + private val repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsListRepository = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = TestingCoroutineDispatcherProvider(), + walletServerBinder = walletServerBinder, + moshi = mockk(), + ) private val testWalletId = UserWalletId("1234567890abcdef") - @Before - fun setup() { - tangemTechApi = mockk() - walletServerBinder = mockk() - appsFlyerStore = mockk() - dispatchers = TestingCoroutineDispatcherProvider() - repository = DefaultWalletsRepository( - appPreferencesStore = appPreferenceStore, - tangemTechApi = tangemTechApi, - userWalletsListRepository = mockk(), - seedPhraseNotificationVisibilityStore = mockk(), - dispatchers = dispatchers, - authProvider = mockk(), - walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = mockk(), - moshi = mockk(), - ) + @AfterEach + fun tearDown() { + clearMocks(tangemTechApi, preferencesDataStore) } - @Test - fun `GIVEN local storage has value WHEN isNotificationsEnabled THEN should return local value`() = runTest { - // GIVEN - val expectedPreferences = """{"${testWalletId.stringValue}":true}""" - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsNotificationsEnabled { - // WHEN - val result = repository.isNotificationsEnabled(testWalletId) + @Test + fun `should return local value when local storage has value`() = runTest { + // Arrange + val expectedPreferences = """{"${testWalletId.stringValue}":true}""" + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences - // THEN - assertThat(result).isTrue() + // Act + val result = repository.isNotificationsEnabled(testWalletId) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `should return false when local storage is empty`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + + // Act + val result = repository.isNotificationsEnabled(testWalletId) + + // Assert + assertThat(result).isFalse() + } } - @Test - fun `GIVEN local storage is empty WHEN isNotificationsEnabled THEN should return false`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetNotificationsEnabled { - // WHEN - val result = repository.isNotificationsEnabled(testWalletId) + @Test + fun `should update local storage when enabled status`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { preferencesDataStore.updateData(any()) } returns mockk() - // THEN - assertThat(result).isFalse() + // Act + repository.setNotificationsEnabled(testWalletId, isEnabled = true) + + // Assert + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `should update local storage when disabled status`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // Act + repository.setNotificationsEnabled(testWalletId, isEnabled = false) + + // Assert + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } } - @Test - fun `GIVEN enabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" - coEvery { preferencesDataStore.updateData(any()) } returns mockk() + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetWalletsInfo { - // WHEN - repository.setNotificationsEnabled(testWalletId, isEnabled = true) - - // THEN - coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } - } - - @Test - fun `GIVEN disabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" - coEvery { preferencesDataStore.updateData(any()) } returns mockk() - - // WHEN - repository.setNotificationsEnabled(testWalletId, isEnabled = false) - - // THEN - coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } - } - - @Test - fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = - runTest { - // GIVEN + @Test + fun `should return converted wallets and update cache when updateCache is true`() = runTest { + // Arrange val applicationId = "test_app_id" val wallet1Id = "1234567890abcdef" val wallet2Id = "fedcba0987654321" val walletResponses = listOf( - WalletResponse( - id = wallet1Id, - notifyStatus = true, - ), - WalletResponse( - id = wallet2Id, - notifyStatus = false, - ), + WalletResponse(id = wallet1Id, notifyStatus = true), + WalletResponse(id = wallet2Id, notifyStatus = false), ) coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) coEvery { preferencesDataStore.updateData(any()) } returns mockk() - // WHEN + // Act val result = repository.getWalletsInfo(applicationId, updateCache = true) - // THEN + // Assert assertThat(result).hasSize(2) assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) assertThat(result[0].isNotificationsEnabled).isTrue() @@ -158,24 +158,20 @@ class DefaultWalletsRepositoryTest { coVerify(exactly = 2) { preferencesDataStore.updateData(any()) } } - @Test - fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = - runTest { - // GIVEN + @Test + fun `should return converted wallets without updating cache when updateCache is false`() = runTest { + // Arrange val applicationId = "test_app_id" val wallet1Id = "1234567890abcdef" val walletResponses = listOf( - WalletResponse( - id = wallet1Id, - notifyStatus = true, - ), + WalletResponse(id = wallet1Id, notifyStatus = true), ) coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) - // WHEN + // Act val result = repository.getWalletsInfo(applicationId, updateCache = false) - // THEN + // Assert assertThat(result).hasSize(1) assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) assertThat(result[0].isNotificationsEnabled).isTrue() @@ -183,152 +179,126 @@ class DefaultWalletsRepositoryTest { coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } coVerify(exactly = 0) { preferencesDataStore.updateData(any()) } } + } - @Test - fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest { - // GIVEN - val applicationId = "test_app_id" - val wallet1Id = "1234567890abcdef" - val wallet2Id = "fedcba0987654321" - val card1PublicKey = "card1_public_key" - val card2PublicKey = "card2_public_key" + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class AssociateWallets { - val userWallets = listOf( - mockk { - every { cardsInWallet } returns setOf(card1PublicKey) - every { walletId } returns UserWalletId(wallet1Id) - every { name } returns "Wallet 1" - }, - mockk { - every { cardsInWallet } returns setOf(card2PublicKey) - every { walletId } returns UserWalletId(wallet2Id) - every { name } returns "Wallet 2" - }, - ) + @Test + fun `should convert and send to API V2`() = runTest { + // Arrange + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val wallet2Id = "fedcba0987654321" - val publicKeys = mapOf( - card1PublicKey to "public_key_1", - card2PublicKey to "public_key_2", - ) - - val authProvider = mockk { - coEvery { getCardsPublicKeys() } returns publicKeys - } - - val accountsFeatureToggles = mockk { - every { isFeatureEnabled } returns false - } - - repository = DefaultWalletsRepository( - appPreferencesStore = appPreferenceStore, - tangemTechApi = tangemTechApi, - userWalletsListRepository = mockk(), - seedPhraseNotificationVisibilityStore = mockk(), - dispatchers = dispatchers, - authProvider = authProvider, - walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, - moshi = mockk(), - ) - - coEvery { appsFlyerStore.get() } returns null - - coEvery { - tangemTechApi.associateApplicationIdWithWallets( - eq(applicationId), - any(), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.associateWallets(applicationId, userWallets) - - // THEN - coVerify(exactly = 1) { - tangemTechApi.associateApplicationIdWithWallets( - applicationId = eq(applicationId), - body = match { body -> - body.size == 2 && - body.any { - it.walletId == wallet1Id && - it.cards!!.any { card -> card.cardPublicKey == "public_key_1" } && - it.name == "Wallet 1" - } && - body.any { - it.walletId == wallet2Id && - it.cards!!.any { card -> card.cardPublicKey == "public_key_2" } && - it.name == "Wallet 2" - } + val userWallets = listOf( + mockk { + every { walletId } returns UserWalletId(wallet1Id) + }, + mockk { + every { walletId } returns UserWalletId(wallet2Id) }, ) + + coEvery { + tangemTechApi.associateApplicationIdWithWalletsV2(eq(applicationId), any()) + } returns ApiResponse.Success(Unit) + + // Act + repository.associateWallets(applicationId, userWallets) + + // Assert + coVerify(exactly = 1) { + tangemTechApi.associateApplicationIdWithWalletsV2( + applicationId = eq(applicationId), + body = match { body -> + body.walletIds.size == 2 && + body.walletIds.contains(wallet1Id) && + body.walletIds.contains(wallet2Id) + }, + ) + } } } - @Test - fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - val promoCode = "PROMO123" - val address = "bc1qexampleaddress" - coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( - PromocodeActivationResponse(status = "activated"), - ) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ActivatePromoCode { - // WHEN - val result = repository.activatePromoCode( - userWalletId = walletId, - promoCode = promoCode, - bitcoinAddress = address - ) - - // THEN - var right: String? = null - var left: ActivatePromoCodeError? = null - result.fold({ left = it }, { right = it }) - assertThat(left).isNull() - assertThat(right).isEqualTo("activated") - - coVerify(exactly = 1) { - tangemTechApi.activatePromoCode( - match { it is PromocodeActivationBody && it.promoCode == promoCode && it.address == address }, + @Test + fun `should return Right with status when API returns success`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + val promoCode = "PROMO123" + val address = "bc1qexampleaddress" + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( + PromocodeActivationResponse(status = "activated"), ) - } - } - @Test - fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - coEvery { tangemTechApi.activatePromoCode(any()) } returns - ApiResponse.Error( + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = promoCode, + bitcoinAddress = address, + ) + + // Assert + var right: String? = null + var left: ActivatePromoCodeError? = null + result.fold({ left = it }, { right = it }) + assertThat(left).isNull() + assertThat(right).isEqualTo("activated") + + coVerify(exactly = 1) { + tangemTechApi.activatePromoCode( + match { it.promoCode == promoCode && it.address == address }, + ) + } + } + + @Test + fun `should return Left InvalidPromoCode when API returns NOT_FOUND`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + @Suppress("UNCHECKED_CAST") + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null), ) as ApiResponse - // WHEN - val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = "PROMO", + bitcoinAddress = "addr", + ) - // THEN - var error: ActivatePromoCodeError? = null - result.fold({ error = it }, { }) - assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode) - } + // Assert + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode) + } - @Test - fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - coEvery { tangemTechApi.activatePromoCode(any()) } returns - ApiResponse.Error( + @Test + fun `should return Left PromocodeAlreadyUsed when API returns CONFLICT`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + @Suppress("UNCHECKED_CAST") + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null), ) as ApiResponse - // WHEN - val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = "PROMO", + bitcoinAddress = "addr", + ) - // THEN - var error: ActivatePromoCodeError? = null - result.fold({ error = it }, { }) - assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed) + // Assert + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed) + } } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index 426a1bc144..cd22c4c1f8 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -14,11 +14,13 @@ import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MissedDerivationsFinderTest { @Test @@ -97,9 +99,8 @@ internal class MissedDerivationsFinderTest { val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) - Truth.assertThat(actual).containsExactly( - ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()), - listOf( + val expected = mapOf( + ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()) to listOf( DerivationConfigV2.derivations(Blockchain.Cardano).values.first(), CardanoUtils.extendedDerivationPath( derivationPath = DerivationPath( @@ -108,7 +109,12 @@ internal class MissedDerivationsFinderTest { ), ), ), + ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()) to listOf( + DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(), + ), ) + + Truth.assertThat(actual).containsExactlyEntriesIn(expected) } @Test diff --git a/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt b/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt deleted file mode 100644 index 289b2a02e6..0000000000 --- a/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.account.featuretoggle - -/** - * Accounts feature toggle - * -[REDACTED_AUTHOR] - */ -interface AccountsFeatureToggles { - - val isFeatureEnabled: Boolean -} \ No newline at end of file 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 2a87c8b14e..a8a0e5cdbb 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 @@ -22,4 +22,8 @@ abstract class SingleAccountSupplier( fun filterPaymentAccount(accountId: AccountId): Flow { return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() } + + fun filterCryptoPortfolioAccount(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/IsAccountsModeEnabledUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt index 2a0bfba4d0..94e6e051af 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 @@ -2,7 +2,6 @@ package com.tangem.domain.account.usecase 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 @@ -18,20 +17,16 @@ import kotlinx.coroutines.flow.* * * @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, ) { @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): Flow { - if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(value = false) - return userWalletsListRepository.loadAndGet() .flatMapLatest { userWallets -> val totalAccountsCountList = getTotalAccountsCountList(userWallets) @@ -43,8 +38,6 @@ class IsAccountsModeEnabledUseCase( } suspend fun invokeSync(): Boolean { - if (!accountsFeatureToggles.isFeatureEnabled) return false - return userWalletsListRepository.userWallets.value.orEmpty() .map { userWallet -> // If the wallet does not support multiple currencies, we consider its account count as 0 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 750b29926b..894d4ec39d 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 @@ -3,7 +3,6 @@ package com.tangem.domain.account.usecase import arrow.core.none 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 @@ -12,7 +11,6 @@ import com.tangem.domain.models.wallet.isMultiCurrency import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -26,47 +24,26 @@ class IsAccountsModeEnabledUseCaseTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) - private val featureToggles: AccountsFeatureToggles = mockk() private val useCase = IsAccountsModeEnabledUseCase( crudRepository = accountsCRUDRepository, userWalletsListRepository = userWalletsListRepository, - accountsFeatureToggles = featureToggles, ) @AfterEach fun tearDown() { - clearMocks(userWalletsListRepository, accountsCRUDRepository, featureToggles) + clearMocks(userWalletsListRepository, accountsCRUDRepository) } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Invoke { - @Test - fun `returns false when feature is disabled`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns false - - // Act - val actual = useCase.invoke().firstOrNull() - - // Assert - Truth.assertThat(actual).isFalse() - - verify(exactly = 1) { featureToggles.isFeatureEnabled } - coVerify(inverse = true) { - userWalletsListRepository.load() - userWalletsListRepository.userWallets - } - } - @Test fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest { // Arrange val wallet = createUserWallet(isMultiCurrency = false) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) // Act @@ -76,7 +53,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets } @@ -89,7 +65,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some()) @@ -100,7 +75,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) @@ -112,7 +86,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none()) @@ -123,7 +96,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) @@ -136,7 +108,6 @@ class IsAccountsModeEnabledUseCaseTest { val wallet1 = createUserWallet(isMultiCurrency = false) val wallet2 = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some()) @@ -147,7 +118,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) @@ -161,25 +131,9 @@ class IsAccountsModeEnabledUseCaseTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class InvokeSync { - @Test - fun `returns false when feature is disabled`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns false - - // Act - val actual = useCase.invokeSync() - - // Assert - Truth.assertThat(actual).isFalse() - - verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { userWalletsListRepository.userWallets.value } - } - @Test fun `returns false when getUserWalletsSync returns empty list`() = runTest { // Arrange - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns emptyList() // Act @@ -189,7 +143,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value } @@ -201,7 +154,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = false) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) // Act @@ -211,7 +163,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value } @@ -223,7 +174,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some() @@ -234,7 +184,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } @@ -245,7 +194,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none() @@ -256,7 +204,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } @@ -268,7 +215,6 @@ class IsAccountsModeEnabledUseCaseTest { val wallet1 = createUserWallet(isMultiCurrency = false) val wallet2 = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some() @@ -279,7 +225,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled 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 a228aceb2e..1f7bea813c 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 @@ -67,8 +67,8 @@ internal object AccountStatusUseCaseModule { fun provideApplyTokenListSortingUseCaseV2( accountsCRUDRepository: AccountsCRUDRepository, dispatchers: CoroutineDispatcherProvider, - ): ApplyTokenListSortingUseCaseV2 { - return ApplyTokenListSortingUseCaseV2( + ): ApplyTokenListSortingUseCase { + return ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = dispatchers, ) @@ -141,8 +141,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListSortingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListSortingUseCaseV2 { - return ToggleTokenListSortingUseCaseV2( + ): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase( dispatchers = dispatchers, ) } @@ -151,8 +151,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListGroupingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCaseV2 { - return ToggleTokenListGroupingUseCaseV2( + ): ToggleTokenListGroupingUseCase { + return ToggleTokenListGroupingUseCase( dispatchers = dispatchers, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt index 5acaea2b98..152ab3ca77 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt @@ -4,7 +4,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.serialization.Serializable -typealias AccountCryptoCurrencies = Map> +typealias AccountCryptoCurrencies = Map> /** * Combines an [Account] with its corresponding [CryptoCurrency]. diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt index 358e3e4ce2..dfe0ea8bf4 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt @@ -16,7 +16,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext class DefaultFlowProducerAppScope @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, + dispatchers: CoroutineDispatcherProvider, private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : FlowProducerScope { @@ -76,10 +76,13 @@ class DefaultFlowProducerTools @Inject constructor( .shareIn( scope = scope, replay = 1, - // params control flow cleanup + // stopTimeoutMillis = 0: upstream collection stops immediately when the last subscriber disappears. + // replayExpirationMillis = 0: replay cache is cleared immediately after upstream stops. + // This ensures that when there are no subscribers, the first subscriber always triggers a fresh + // upstream collection instead of receiving a stale replay. started = SharingStarted.WhileSubscribed( - stopTimeoutMillis = 5_000, - replayExpirationMillis = 30_000, + stopTimeoutMillis = 0, + replayExpirationMillis = 0, ), ) } 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 be5c3f927d..5250d458de 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 @@ -164,7 +164,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency: MutableSharedFlow>, ): Flow> { val walletId = userWallet.walletId - val networkStatusFlow: SharedFlow> = networkStatusFlow(walletId, flattenCurrency) + val networkStatusFlow: SharedFlow> = networkStatusFlow(walletId) .shareIn(this, started = SharingStarted.Eagerly, replay = 1) val stakingBalanceFlow: SharedFlow>> = stakingFlow(userWallet) .shareIn(this, started = SharingStarted.Eagerly, replay = 1) @@ -213,27 +213,10 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo } } - private fun networkStatusFlow( - walletId: UserWalletId, - flattenCurrency: MutableSharedFlow>, - ): Flow> = channelFlow { - val currencyCount = flattenCurrency - .map { map -> map.size } - .stateIn(this, SharingStarted.Eagerly, 0) - + private fun networkStatusFlow(walletId: UserWalletId): Flow> = networkStatusSupplier(MultiNetworkStatusProducer.Params(walletId)) - // todo accounts high frequency, investigate better debounce - .debounce { - val count = currencyCount.value - @Suppress("MagicNumber") when { - count in 10..25 -> 50L - count > 25 -> 100L - else -> 0 - } - } .mapLatest { statuses -> statuses.associateBy { status -> status.network.id } } - .distinctUntilChanged().collect { result -> channel.send(result) } - } + .distinctUntilChanged() private fun stakingFlow(wallet: UserWallet): Flow>> = if (!wallet.isMultiCurrency) { 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/ApplyTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt index 6dc53cca47..55e21d936c 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/ApplyTokenListSortingUseCase.kt @@ -25,7 +25,7 @@ private typealias SortingErrorByAccountId = MutableMap + errors[account.accountId] = error return@map account } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt index 32e1b28366..648daca038 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/ToggleTokenListGroupingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListGroupingUseCaseV2( +class ToggleTokenListGroupingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt index ccf2416368..18bfbc35a1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListSortingUseCaseV2( +class ToggleTokenListSortingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { 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 af71ec2c02..bd340dc150 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 @@ -30,7 +30,7 @@ internal class ApplyTokenListSortingUseCaseTest { private val accountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = ApplyTokenListSortingUseCaseV2( + private val useCase = ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt similarity index 98% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt index 9dadb85e4f..9e3b423a62 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListGroupingUseCaseV2Test { +class ToggleTokenListGroupingUseCaseTest { - private val useCase = ToggleTokenListGroupingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListGroupingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt similarity index 97% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt index 344dc8641e..c7808be2d4 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListSortingUseCaseV2Test { +class ToggleTokenListSortingUseCaseTest { - private val useCase = ToggleTokenListSortingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListSortingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/earn/build.gradle.kts b/domain/earn/build.gradle.kts index f72d726fea..380db663f4 100644 --- a/domain/earn/build.gradle.kts +++ b/domain/earn/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { api(projects.domain.core) api(projects.domain.models) api(projects.core.pagination) + implementation(projects.domain.account) 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/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index 1988d0e166..e6a93c5605 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -1,26 +1,29 @@ package com.tangem.domain.earn.usecase import arrow.core.Either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier 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.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged /** - * Observes earn networks with [EarnNetwork.isAdded] enriched from user's wallets - * via [multiNetworkStatusSupplier]. Single entry point for all/mine filtering. + * Observes earn networks with [EarnNetwork.isAdded] enriched from user's active (non-archived) + * accounts via [multiAccountListSupplier]. Single entry point for all/mine filtering. + * + * Uses [MultiAccountListSupplier] so that only networks from active accounts are considered; + * archived accounts are not included in [AccountList.accounts]. */ class GetEarnNetworksUseCase( private val earnRepository: EarnRepository, + private val multiAccountListSupplier: MultiAccountListSupplier, private val userWalletsListRepository: UserWalletsListRepository, - private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, ) { operator fun invoke(): Flow { @@ -36,24 +39,24 @@ class GetEarnNetworksUseCase( }.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() } - } + return combine( + multiAccountListSupplier(), + userWalletsListRepository.userWallets, + ) { accountLists, wallets -> + val unlockedWalletsId = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .mapTo(HashSet()) { it.walletId } + + if (unlockedWalletsId.isEmpty()) { + return@combine emptySet() } + + accountLists + .filter { it.userWalletId in unlockedWalletsId } + .flatMap(AccountList::flattenCurrencies) + .mapTo(HashSet()) { it.network.backendId } + } } } \ No newline at end of file diff --git a/domain/kyc/models/.gitignore b/domain/kyc/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/kyc/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/kyc/models/build.gradle.kts b/domain/kyc/models/build.gradle.kts new file mode 100644 index 0000000000..6b18f3f83f --- /dev/null +++ b/domain/kyc/models/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 1a8068cfb5..840894f1ad 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -17,4 +17,5 @@ object AnalyticsHandlersLogConfig { val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isCustomerIoLogEnabled: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt deleted file mode 100644 index b82f1cf58a..0000000000 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.managetokens - -import arrow.core.Either -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.managetokens.repository.CustomTokensRepository -import com.tangem.domain.models.wallet.UserWalletId - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - customCurrency: ManagedCryptoCurrency.Custom, - ): Either { - return Either.catch { - repository.removeCurrency(userWalletId, customCurrency) - } - } -} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt deleted file mode 100644 index 3fd2900bd7..0000000000 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.tangem.domain.managetokens - -import arrow.core.Either -import arrow.core.flatten -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.managetokens.repository.CustomTokensRepository -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class SaveManagedTokensUseCase( - private val customTokensRepository: CustomTokensRepository, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val derivationsRepository: DerivationsRepository, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, - private val parallelUpdatingScope: CoroutineScope, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - currenciesToAdd: Map>, - currenciesToRemove: Map>, - ): Either = Either.catch { - if (currenciesToRemove.isNotEmpty()) { - val removingCurrencies = currenciesToRemove.mapToCryptoCurrencies(userWalletId) - - currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removingCurrencies) - - removeCurrenciesFromWalletManager(userWalletId = userWalletId, currencies = removingCurrencies) - } - - if (currenciesToAdd.isNotEmpty()) { - derivationsRepository.derivePublicKeysByNetworks( - userWalletId = userWalletId, - networks = currenciesToAdd.values.flatten(), - ) - - val addingCurrencies = currenciesToAdd.mapToCryptoCurrencies(userWalletId) - - val savedCurrencies = currenciesRepository.addCurrenciesCache( - userWalletId = userWalletId, - currencies = addingCurrencies, - ) - - parallelUpdatingScope.launch { - withContext(NonCancellable) { - syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies) - - launch { - refreshUpdatedNetworks( - userWalletId = userWalletId, - addedCurrencies = savedCurrencies, - ) - } - launch { - refreshUpdatedStakingBalances( - userWalletId = userWalletId, - addedCurrencies = savedCurrencies, - ) - } - launch { refreshUpdatedQuotes(addedCurrencies = savedCurrencies) } - } - } - } - } - - private suspend fun removeCurrenciesFromWalletManager( - userWalletId: UserWalletId, - currencies: List, - ) { - walletManagersFacade.remove( - userWalletId = userWalletId, - networks = currencies - .filterIsInstance() - .mapTo(hashSetOf(), CryptoCurrency::network), - ) - - walletManagersFacade.removeTokens( - userWalletId = userWalletId, - tokens = currencies.filterIsInstance().toSet(), - ) - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - addedCurrencies: List, - ) { - val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - } - - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), - ) - } - - private suspend fun Map>.mapToCryptoCurrencies( - userWalletId: UserWalletId, - ): List { - return flatMap { (token, networks) -> - token.availableNetworks - .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } - .map { sourceNetwork -> - when (sourceNetwork) { - is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( - managedCryptoCurrency = token, - sourceNetwork = sourceNetwork, - rawId = CryptoCurrency.RawID(token.id.value), - ) - is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( - userWalletId = userWalletId, - networkId = sourceNetwork.id, - derivationPath = sourceNetwork.network.derivationPath, - ) - } - } - } - } -} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt index 8ac66e350c..fad77173a5 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt @@ -3,24 +3,10 @@ package com.tangem.domain.managetokens.model import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId -sealed interface ManageTokensListConfig { - +data class ManageTokensListConfig( + val accountId: AccountId?, + val searchText: String?, +) { val userWalletId: UserWalletId? - val searchText: String? - - // old way - data class Wallet( - override val userWalletId: UserWalletId?, - override val searchText: String?, - ) : ManageTokensListConfig - - // new way - data class Account( - val accountId: AccountId?, - override val searchText: String?, - ) : ManageTokensListConfig { - - override val userWalletId: UserWalletId? - get() = accountId?.userWalletId - } + get() = accountId?.userWalletId } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index 2f6cb3035e..219d138628 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -43,9 +43,6 @@ interface CustomTokensRepository { formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) - suspend fun convertToCryptoCurrency( userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt deleted file mode 100644 index 08b7e83e21..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.tangem.domain.markets - -import arrow.core.Either -import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * Use case for saving tokens from Markets - * - * @property derivationsRepository derivations repository - * @property marketsTokenRepository markets token repository - * @property currenciesRepository currencies repository - * -[REDACTED_AUTHOR] - */ -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class SaveMarketTokensUseCase( - private val derivationsRepository: DerivationsRepository, - private val marketsTokenRepository: MarketsTokenRepository, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, - private val parallelUpdatingScope: CoroutineScope, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - tokenMarketParams: TokenMarketParams, - addedNetworks: Set, - removedNetworks: Set, - ): Either = Either.catch { - if (removedNetworks.isNotEmpty()) { - val removedCurrencies = removedNetworks.mapNotNull { network -> - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = network, - ) - } - - currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removedCurrencies) - } - - if (addedNetworks.isNotEmpty()) { - derivationsRepository.derivePublicKeysByNetworkIds( - userWalletId = userWalletId, - networkIds = addedNetworks.map { Network.RawID(it.networkId) }, - accountIndex = DerivationIndex.Main, - ) - - val addedCurrencies = addedNetworks.mapNotNull { network -> - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = network, - accountIndex = DerivationIndex.Main, - ) - } - - val savedCurrencies = currenciesRepository.addCurrenciesCache( - userWalletId = userWalletId, - currencies = addedCurrencies, - ) - - parallelUpdatingScope.launch { - withContext(NonCancellable) { - syncTokens(userWalletId, savedCurrencies) - - launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) } - launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) } - launch { refreshUpdatedQuotes(savedCurrencies) } - } - } - } - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - existingCurrencies: List, - ) { - val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - } - - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), - ) - } -} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt index 07b9e3d022..a140670520 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt @@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable /** * Temporary wrapper over ID to support a gradual migration between two modes: * - * - [Wallet] — legacy flow, wallet design; used when the [AccountsFeatureToggles] is disabled. - * - [Account] — new flow, wallet/account design; used when the [AccountsFeatureToggles] is enabled. + * - [Wallet] — legacy flow, wallet design. + * - [Account] — new flow, wallet/account design. * - * ⚠️ When an [Account] you must verify the current app mode with [IsAccountsModeEnabledUseCase] - * and then use wallet/account design + * ⚠️ When using an [Account], you must verify the current app mode with [IsAccountsModeEnabledUseCase], + * then use the wallet/account design. * * Intended to be removed after the full migration to the new mode. */ diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt new file mode 100644 index 0000000000..7297bc7c87 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.models.kyc + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +private const val APPROVED_KYC_STATUS = "approved" +private const val IN_PROGRESS_KYC_STATUS = "in_progress" +private const val DECLINED_KYC_STATUS = "declined" + +@JsonClass(generateAdapter = false) +enum class KycStatus { + /** Initial state */ + @Json(name = "init") + INIT, + + /** Performing the check */ + @Json(name = "in_progress") + PENDING, + + /** SumSub approved */ + @Json(name = "approved") + APPROVED, + + /** The check failed, documents rejected */ + @Json(name = "declined") + REJECTED, + + ; + + companion object { + + fun fromString(status: String?, default: KycStatus = INIT): KycStatus { + return when (status?.lowercase()) { + IN_PROGRESS_KYC_STATUS -> PENDING + DECLINED_KYC_STATUS -> REJECTED + APPROVED_KYC_STATUS -> APPROVED + else -> default + } + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt new file mode 100644 index 0000000000..dc5417d014 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.models.wallet + +/** + * Represents the icon of a user wallet, which can be of different types such as hot, stub, default, or colored. + */ +sealed class UserWalletIcon { + data object Hot : UserWalletIcon() + data class Stub(val cardsCount: Int) : UserWalletIcon() + + data class Default( + val isRing: Boolean, + val cardsCount: Int, + ) : UserWalletIcon() + + data class Colored( + val isRing: Boolean, + val mainColor: String, + val secondColor: String? = null, + val thirdColor: String? = null, + ) : UserWalletIcon() +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index 95b2fd53cb..f8432faa75 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -1,6 +1,5 @@ package com.tangem.domain.nft -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -8,52 +7,42 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.WalletNFTCollections import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* class GetNFTCollectionsUseCase( - private val currenciesRepository: CurrenciesRepository, private val nftRepository: NFTRepository, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, ) { - @Deprecated("Use invokeForAccounts instead") @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> = - if (accountsFeatureToggles.isFeatureEnabled) { - invokeForAccounts(userWalletId).map { it.flattenCollections } - } else { - currenciesRepository - .getWalletCurrenciesUpdates(userWalletId) - .flatMapLatest { - nftCollections(userWalletId, it) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun invokeForAccounts(userWalletId: UserWalletId): Flow { - fun Account.flowOfNFTCollections(): Flow>>? { - val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() - if (currencies.isEmpty()) return null - return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList()) - .map { nfts -> this to nfts } - } - + operator fun invoke(userWalletId: UserWalletId): Flow { return singleAccountListSupplier(userWalletId) - .mapLatest { statusList -> statusList.accounts.mapNotNull { it.flowOfNFTCollections() } } - .flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } } + .mapLatest { statusList -> statusList.accounts.mapNotNull(::flowOfNFTCollections) } + .flatMapLatest { flows -> + combine(flows) { WalletNFTCollections(it.toMap()) } + } } - private fun nftCollections( + private fun flowOfNFTCollections(account: Account): Flow>>? { + val currencies = (account as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() + + if (currencies.isEmpty()) return null + + return getNftCollections(userWalletId = account.userWalletId, cryptoCurrencies = currencies.toList()) + .map { nfts -> account to nfts } + } + + private fun getNftCollections( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow> { val networks = cryptoCurrencies - .map { cryptoCurrency -> cryptoCurrency.network } + .map(CryptoCurrency::network) .distinct() + if (networks.isEmpty()) return flowOf(emptyList()) + return nftRepository.observeCollections(userWalletId, networks) } } \ No newline at end of file diff --git a/domain/offramp/.gitignore b/domain/offramp/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/offramp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/offramp/build.gradle.kts b/domain/offramp/build.gradle.kts new file mode 100644 index 0000000000..c2d05ca8fe --- /dev/null +++ b/domain/offramp/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + /** Domain modules */ + api(projects.domain.core) + api(projects.domain.models) + + /** Test libraries */ + testImplementation(projects.test.core) + testRuntimeOnly(deps.test.junit5.engine) +} diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt new file mode 100644 index 0000000000..75b764ac2d --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.offramp + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.offramp.repository.OfframpRepository + +/** + * Use case for getting offramp (sell crypto) URL + * + * @property offrampRepository repository for offramp operations + */ +class GetOfframpUrlUseCase( + private val offrampRepository: OfframpRepository, +) { + + operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrencyCode: String): Either = + either { + val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ensure(walletAddress != null) { Error.WalletAddressNotFound } + + val url = offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrencyStatus.currency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + ensure(url != null) { Error.UrlNotAvailable } + + url + } + + /** Offramp use case errors */ + sealed class Error { + /** Wallet address not found in currency status */ + data object WalletAddressNotFound : Error() + + /** Offramp URL is not available for this currency */ + data object UrlNotAvailable : Error() + } +} \ No newline at end of file diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt new file mode 100644 index 0000000000..0fdfca218b --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.offramp.repository + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * Repository for offramp (sell crypto) operations + */ +interface OfframpRepository { + + /** + * Get offramp (sell) URL for the given cryptocurrency + * + * @param cryptoCurrency crypto currency to sell + * @param fiatCurrencyCode fiat currency code (e.g., "USD", "EUR") + * @param walletAddress wallet address for the refund + * @return URL for offramp service or null if not available + */ + fun getOfframpUrl(cryptoCurrency: CryptoCurrency, fiatCurrencyCode: String, walletAddress: String): String? +} \ No newline at end of file diff --git a/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt new file mode 100644 index 0000000000..2ae52dafbe --- /dev/null +++ b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt @@ -0,0 +1,127 @@ +package com.tangem.domain.offramp + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.offramp.repository.OfframpRepository +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetOfframpUrlUseCaseTest { + + private val offrampRepository: OfframpRepository = mockk() + private val useCase = GetOfframpUrlUseCase(offrampRepository) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val appCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + private val expectedUrl = "https://moonpay.com/sell?address=$walletAddress" + + @BeforeEach + fun resetMocks() { + clearMocks(offrampRepository) + } + + @Test + fun `invoke should return url when wallet address and url are available`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns expectedUrl + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isRight()).isTrue() + assertThat(result.getOrNull()).isEqualTo(expectedUrl) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + @Test + fun `invoke should return WalletAddressNotFound error when network address is null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(networkAddress = null) + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.WalletAddressNotFound) + + verify(exactly = 0) { + offrampRepository.getOfframpUrl(any(), any(), any()) + } + } + + @Test + fun `invoke should return UrlNotAvailable error when repository returns null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns null + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.UrlNotAvailable) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + private fun createCryptoCurrencyStatus( + walletAddress: String? = null, + networkAddress: NetworkAddress? = null, + ): CryptoCurrencyStatus { + val resolvedNetworkAddress = networkAddress ?: walletAddress?.let { address -> + mockk { + every { defaultAddress } returns mockk { + every { value } returns address + } + } + } + + val statusValue: CryptoCurrencyStatus.Value = mockk { + every { this@mockk.networkAddress } returns resolvedNetworkAddress + } + + return mockk { + every { currency } returns cryptoCurrency + every { value } returns statusValue + } + } +} + diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt index fa08534edf..3553692065 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -1,7 +1,5 @@ package com.tangem.domain.staking.toggles interface StakingFeatureToggles { - val isTonStakingEnabled: Boolean - val isCardanoStakingEnabled: Boolean val isEthStakingEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c2a83d7894..53e21909a4 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.card) implementation(projects.domain.staking) + implementation(projects.domain.visa) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt index 4d139661e9..6206a29df4 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt @@ -11,9 +11,12 @@ class TokenExchangeAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Token", event, params) { - class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent( + class CexTxStatusOpened(token: String, provider: String) : TokenScreenAnalyticsEvent( event = "Swap Status Opened", - params = mapOf(TOKEN_PARAM to token), + params = mapOf( + TOKEN_PARAM to token, + PROVIDER to provider, + ), ) class CexTxStatusChanged(token: String, status: String, provider: String) : TokenScreenAnalyticsEvent( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt deleted file mode 100644 index 7473e2eb33..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.getOrElse -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.right -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope - -/** - * A use case for adding multiple cryptocurrencies to a user's wallet. - * - * This use case interacts with the underlying repositories to both add currencies and refresh - * network statuses, particularly after the addition of new tokens. - */ -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class AddCryptoCurrenciesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val walletManagersFacade: WalletManagersFacade, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val stakingIdFactory: StakingIdFactory, -) { - - /** - * Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param cryptoCurrency Token to add. - * @param network Network where we add - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency.Token, - network: Network, - ): Either = either { - val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) - invoke(userWalletId = userWalletId, currency = tokenToAdd) - } - - /** - * Adds a [currency] to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param currency Cryptocurrency to add. - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = - either { - val existingCurrencies = catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }, - catch = ::raise, - ) - val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - - val addedCurrencies = addCurrencies(userWalletId, currencyToAdd) - - coroutineScope { - syncTokens(userWalletId, addedCurrencies) - - awaitAll( - async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, - async { refreshUpdatedStakingBalances(userWalletId, currencyToAdd) }, - async { refreshUpdatedQuotes(currencyToAdd) }, - ) - } - } - - suspend operator fun invoke( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): Either = either { - val existingCurrencies = catch( - block = { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - .toList() - }, - catch = ::raise, - ) - - val foundToken = existingCurrencies - .filterIsInstance() - .firstOrNull { token -> - token.network.backendId == networkId && - !token.isCustom && - token.contractAddress.equals(contractAddress, true) - } - if (foundToken != null) { - return@either foundToken - } - val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - val addedCurrencies = addCurrencies(userWalletId, tokenToAdd) - - coroutineScope { - syncTokens(userWalletId = userWalletId, addedCurrencies = addedCurrencies) - - awaitAll( - async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, - async { refreshUpdatedStakingBalances(userWalletId, tokenToAdd) }, - async { refreshUpdatedQuotes(tokenToAdd) }, - ) - } - - tokenToAdd - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - /** - * Refreshes the network statuses for tokens that have corresponding coins in the - * [existingCurrencies] list. - */ - private suspend fun refreshUpdatedNetworks( - userWalletId: UserWalletId, - currencyToAdd: CryptoCurrency, - existingCurrencies: List, - ) { - val networksToUpdate = currencyToAdd.takeIf { currency -> - currency is CryptoCurrency.Token && hasCoinForToken(existingCurrencies, currency) - } - ?.network - - val networkToUpdate = currencyToAdd.takeIf { - !existingCurrencies.map(CryptoCurrency::network).contains(it.network) - } - ?.network - - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = setOfNotNull(networksToUpdate, networkToUpdate), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - addedCurrency: CryptoCurrency, - ): Either = either { - val stakingId = stakingIdFactory.create( - userWalletId = userWalletId, - currencyId = addedCurrency.id, - network = addedCurrency.network, - ) - .getOrElse { error -> - when (error) { - is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$error")) - StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() - } - - return@either - } - - singleStakingBalanceFetcher( - params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), - ) - .bind() - } - - private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), - appCurrencyId = null, - ), - ) - } - - private suspend fun Raise.createTokenCurrency( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): CryptoCurrency.Token { - return catch( - block = { - currenciesRepository.createTokenCurrency( - userWalletId = userWalletId, - contractAddress = contractAddress, - networkId = networkId, - ) - }, - catch = { - raise(it) - }, - ) - } - - private suspend fun Raise.addCurrencies( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): List { - return catch( - block = { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, - catch = ::raise, - ) - } - - /** - * Determines if the [existingCurrencies] list contains a coin that corresponds - * to the given [token]. - */ - private fun hasCoinForToken(existingCurrencies: List, token: CryptoCurrency.Token): Boolean { - return existingCurrencies.any { currency -> - currency is CryptoCurrency.Coin && currency.network == token.network - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt deleted file mode 100644 index e98428eee2..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptyListOrNull -import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext - -@Deprecated("Use ApplyAccountListSortingUseCase") -class ApplyTokenListSortingUseCase( - private val currenciesRepository: CurrenciesRepository, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - sortedTokensIds: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ): Either { - return either { - val storedCurrencies = getCurrencies(userWalletId) - val isSortingTypeChanged = checkIsCurrenciesSortedByBalance(userWalletId) != isSortedByBalance - val isGroupingTypeChanged = checkIsCurrenciesGroupedByNetwork(userWalletId) != isGroupedByNetwork - - val sortedCurrencies = sortTokens(sortedTokensIds, storedCurrencies) - - if (storedCurrencies != sortedCurrencies || isSortingTypeChanged || isGroupingTypeChanged) { - applySorting( - userWalletId = userWalletId, - currencies = sortedCurrencies, - isGrouped = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } - } - } - - private suspend fun Raise.checkIsCurrenciesSortedByBalance(userWalletId: UserWalletId) = - catch( - block = { currenciesRepository.isTokensSortedByBalance(userWalletId).firstOrNull() == true }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - private suspend fun Raise.checkIsCurrenciesGroupedByNetwork(userWalletId: UserWalletId) = - catch( - block = { currenciesRepository.isTokensGrouped(userWalletId).firstOrNull() == true }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - private suspend fun Raise.sortTokens( - sortedCurrenciesIds: List, - unsortedCurrencies: List, - ): List = withContext(dispatchers.default) { - val nonEmptySortedTokensIds = ensureNotNull(sortedCurrenciesIds.toNonEmptySetOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - - val sortedTokens = sortedMapOf() - - unsortedCurrencies.distinct().forEach { currency -> - val index = nonEmptySortedTokensIds.indexOfFirst { currencyId -> - currencyId == currency.id - } - - if (index >= 0) { - sortedTokens[index] = currency - } else { - raise(TokenListSortingError.UnableToSortTokenList) - } - } - - ensureNotNull(sortedTokens.values.toNonEmptyListOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - } - - private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): List { - val tokens = catch( - block = { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - return ensureNotNull(tokens.toNonEmptyListOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - } - - private suspend fun Raise.applySorting( - userWalletId: UserWalletId, - currencies: List, - isGrouped: Boolean, - isSortedByBalance: Boolean, - ) = withContext(dispatchers.io) { - catch( - block = { currenciesRepository.saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt deleted file mode 100644 index 9bba447d40..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -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.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -/** - * Get crypto currency statuses by raw ID for all wallets - * - * @property currenciesRepository currencies repository - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -class GetAllWalletsCryptoCurrencyStatusesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val dispatchers: CoroutineDispatcherProvider, - private val currencyStatusOperations: BaseCurrencyStatusOperations, -) { - - /** - * Get crypto currency statuses by [currencyRawId] for all wallets - * - * @param currencyRawId currency raw ID - */ - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke( - currencyRawId: CryptoCurrency.RawID, - ): Flow>>> { - return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) - .flatMapLatest { userWalletsWithCurrencies: Map> -> - val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> - val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> - currencyStatusOperations.getCurrencyStatusFlow(userWallet.walletId, cryptoCurrency) - .map { it.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } - } - - combine(currencyStatusFlows) { statuses -> userWallet to statuses.toList() } - .onEmpty { emit(userWallet to emptyList()) } - } - - combine(walletStatusFlows) { it.toMap() } - .onEmpty { emit(emptyMap()) } - } - .flowOn(dispatchers.io) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt deleted file mode 100644 index 839334727f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.repository.CurrenciesRepository - -@Deprecated("Use MultiWalletCryptoCurrenciesSupplier") -class GetCryptoCurrenciesUseCase( - private val currenciesRepository: CurrenciesRepository, -) { - - /** - * Retrieves the list of cryptocurrencies within a multi-currency wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke(userWalletId: UserWalletId): Either> { - return Either.catch { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - }.mapLeft(CurrencyStatusError::DataError) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt deleted file mode 100644 index 042f4d257f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.domain.tokens - -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.lceError -import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.core.utils.toLce -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest - -class GetTokenListUseCase( - private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrencyStatusOperations, -) { - - @OptIn(ExperimentalCoroutinesApi::class) - fun launch(userWalletId: UserWalletId): LceFlow { - return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId) - .transformLatest { maybeCurrencies -> - maybeCurrencies.fold( - ifLoading = { maybeContent -> - if (maybeContent != null) { - emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true)) - } else { - emit(lceLoading()) - } - }, - ifContent = { content -> - emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false)) - }, - ifError = { error -> emit(error.lceError()) }, - ) - } - } - - private fun createTokenListLce( - userWalletId: UserWalletId, - currencies: List, - isCurrenciesLoading: Boolean, - ): LceFlow { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = currencies, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList - .mapLeft(TokenListOperations.Error::mapToTokenListError) - .toLce(isCurrenciesLoading) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt deleted file mode 100644 index 11760b5e2f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.atomic.update -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.utils.lceContent -import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap - -class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrencyStatusOperations, -) { - - private val walletBalanceCache = ConcurrentHashMap() - - operator fun invoke( - userWalletsIds: Collection, - ): LceFlow> { - val flows = userWalletsIds.distinct() - .map { userWalletId -> - invoke(userWalletId).map { maybeBalance -> - userWalletId to maybeBalance - } - } - - return combine(flows) { balances -> - lce { - balances.fold(mutableMapOf()) { acc, (userWalletId, maybeBalance) -> - val balance = maybeBalance.fold( - ifLoading = { TotalFiatBalance.Loading }, - ifContent = { it }, - ifError = { - Timber.e("failed to load balances with error: $it") - TotalFiatBalance.Failed - }, - ) - - isLoading.update { it || balance is TotalFiatBalance.Loading } - - acc[userWalletId] = balance - acc - } - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): LceFlow { - return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId).map(::createBalance) - .distinctUntilChanged() - .onStart { - val cachedBalance = walletBalanceCache[userWalletId] - - if (cachedBalance != null) { - emit(cachedBalance.lceContent()) - } - } - .mapLatest { lceBalance -> - val cachedBalance = walletBalanceCache[userWalletId] - - if (cachedBalance == null) { - lceBalance.onContent { content -> - if (content is TotalFiatBalance.Loaded) { - walletBalanceCache.put(userWalletId, content) - } - } - - lceBalance - } else { - val content = lceBalance.getOrNull(isPartialContentAccepted = false) - - if (content is TotalFiatBalance.Loaded && content != cachedBalance) { - walletBalanceCache.put(userWalletId, content) - - lceBalance - } else { - cachedBalance.lceContent() - } - } - } - .distinctUntilChanged() - } - - private fun createBalance( - maybeStatuses: Lce>, - ): Lce = lce { - val statuses = when (maybeStatuses) { - is Lce.Content -> maybeStatuses.content - is Lce.Error -> raise(maybeStatuses) - is Lce.Loading -> { - val content = maybeStatuses.partialContent - - if (content == null) { - isLoading.set(true) - - raise(lceLoading()) - } else { - content - } - } - } - - TotalFiatBalanceCalculator.calculate( - statuses = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() }, - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt deleted file mode 100644 index 1ff28dd2af..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.catch -import arrow.core.raise.either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.remove.RemoveCurrencyError -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -class RemoveCurrencyUseCase( - private val currenciesRepository: CurrenciesRepository, - private val walletManagersFacade: WalletManagersFacade, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, -) { - - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend operator fun invoke( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Either { - return either { - if (hasLinkedTokens(userWalletId, currency)) { - raise(RemoveCurrencyError.HasLinkedTokens) - } - - catch( - block = { - currenciesRepository.removeCurrency(userWalletId, currency) - - when (currency) { - is CryptoCurrency.Coin -> { - walletManagersFacade.remove(userWalletId, setOf(currency.network)) - } - is CryptoCurrency.Token -> { - walletManagersFacade.removeTokens(userWalletId, setOf(currency)) - } - } - }, - catch = { raise(RemoveCurrencyError.DataError(it)) }, - ) - } - } - - suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { - return when (currency) { - is CryptoCurrency.Coin -> { - val walletCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - - walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network } - } - is CryptoCurrency.Token -> false - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt deleted file mode 100644 index 11bdea2a34..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListGroupingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - when (tokenList) { - is TokenList.GroupedByNetwork -> ungroupTokens(tokenList) - is TokenList.Ungrouped -> groupTokens(tokenList) - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - } - } - } - } - - private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { - validate(tokenList) - - return TokenListFactory.createGroupedByNetwork(tokenList) - } - - private fun Raise.ungroupTokens(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped { - validate(tokenList) - - return TokenListFactory.createUngrouped(tokenList) - } - - private fun Raise.validate(tokenList: TokenList) { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - ensure(tokenList.flattenCurrencies().isNotEmpty()) { - TokenListSortingError.TokenListIsEmpty - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt deleted file mode 100644 index 2bc37f5bb1..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListSortingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - TokenListFactory.create( - statuses = tokenList.flattenCurrencies(), - groupType = when (tokenList) { - is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK - is TokenList.Ungrouped -> TokensGroupType.NONE - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - }, - sortType = TokensSortType.BALANCE, - ) - } - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index a2eeb1e0a6..c281c5edd4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -5,4 +5,6 @@ package com.tangem.domain.tokens * [REDACTED_AUTHOR] */ -interface TokensFeatureToggles \ No newline at end of file +interface TokensFeatureToggles { + val isMultiAddressUtxoEnabled: Boolean +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 081d02f18f..51e08ac450 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -2,7 +2,6 @@ package com.tangem.domain.tokens.error.mapper import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenListError { return when (this) { @@ -15,12 +14,4 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyStakingBalances, -> TokenListError.EmptyTokens } -} - -internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { - return when (this) { - is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) - is TokenListOperations.Error.UnableToSortTokenList -> - TokenListError.UnableToSortTokenList(this.unsortedTokenList) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt deleted file mode 100644 index 1cf1651c9d..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.tokens.legacy - -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import org.rekotlin.Action - -sealed class TradeCryptoAction : Action { - - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - - data class Sell( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - ) : TradeCryptoAction() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 10043d51ca..117b907f78 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -3,7 +3,6 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,7 +27,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -42,7 +40,7 @@ import kotlinx.coroutines.flow.* [REDACTED_AUTHOR] */ @Suppress("LargeClass", "LongParameterList") -abstract class BaseCurrencyStatusOperations( +class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -56,10 +54,6 @@ abstract class BaseCurrencyStatusOperations( private val currencyStatusProxyCreator = CurrencyStatusProxyCreator() - abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> - - protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> - suspend fun getCurrencyStatusFlow( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, @@ -389,6 +383,14 @@ abstract class BaseCurrencyStatusOperations( .bind() } + private fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + } + private suspend fun getStakingBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, @@ -439,7 +441,7 @@ abstract class BaseCurrencyStatusOperations( ) } - protected fun getIds(currencies: List): Pair, NonEmptySet> { + private fun getIds(currencies: List): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> currency.id to currency.network } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt deleted file mode 100644 index a1561d6d2e..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ /dev/null @@ -1,340 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.* -import arrow.core.raise.recover -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.core.utils.lceContent -import com.tangem.domain.core.utils.lceError -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.network.NetworkStatus -import com.tangem.domain.models.network.getAddress -import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.networks.single.SingleNetworkStatusProducer -import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.single.SingleQuoteStatusProducer -import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier -import com.tangem.domain.staking.single.SingleStakingBalanceProducer -import com.tangem.domain.staking.single.SingleStakingBalanceSupplier -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -@Suppress("LongParameterList", "LargeClass") -class CachedCurrenciesStatusesOperations( - private val currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, - multiStakingBalanceSupplier: MultiStakingBalanceSupplier, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val stakingIdFactory: StakingIdFactory, -) : BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleStakingBalanceSupplier = singleStakingBalanceSupplier, - multiStakingBalanceSupplier = multiStakingBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, -) { - - override fun getCurrenciesStatuses( - userWalletId: UserWalletId, - ): LceFlow> { - return transformToCurrenciesStatuses( - userWalletId = userWalletId, - currenciesFlow = getCurrencies(userWalletId), - ) - } - - @Suppress("LongMethod") - @OptIn(ExperimentalCoroutinesApi::class) - private fun transformToCurrenciesStatuses( - userWalletId: UserWalletId, - currenciesFlow: EitherFlow>, - ): LceFlow> = lceFlow { - val prevStatuses = MutableStateFlow(value = emptyList()) - - currenciesFlow.flatMapLatest { maybeCurrencies -> - val currencies = maybeCurrencies - .getOrElse { return@flatMapLatest flowOf(it.lceError()) } - .toNonEmptyListOrNull() - - if (currencies.isNullOrEmpty()) { - prevStatuses.value = emptyList() - return@flatMapLatest flowOf(TokenListError.EmptyTokens.lceError()) - } - - // This is only 'true' when the flow here is empty, such as during initial loading - if (isLoading.get()) { - val loadingCurrencies = createCurrenciesStatuses( - currencies = currencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeStakingBalances = null, - isUpdating = true, - ) - - loadingCurrencies.getOrNull()?.let { prevStatuses.value = it } - - send(loadingCurrencies) - } - - val (networks, currenciesIds) = getIds(currencies) - - fun createCurrenciesStatuses( - maybeQuotes: Either>, - maybeNetworkStatuses: Either>, - maybeStakingBalances: Either>, - isUpdating: Boolean, - ) = createCurrenciesStatuses( - currencies = currencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworkStatuses, - maybeStakingBalances = maybeStakingBalances, - isUpdating = isUpdating, - ) - - // removing token - val prevStatusesValue = prevStatuses.value - if (prevStatusesValue.size - currencies.size == 1) { - val removed = prevStatusesValue.map { it.currency } - currencies - - return@flatMapLatest flowOf( - prevStatusesValue.filter { it.currency !in removed }.lceContent(), - ) - } - - val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) - - combine( - flow = getQuotes(currenciesIds), - flow2 = networksStatusesUpdates, - flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses -> - val networksStatuses = maybeNetworksStatuses.getOrNull() - - val currenciesAddresses = if (networksStatuses == null) { - emptyMap() - } else { - currencies.associate { currency -> - val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } - - currency.id to networkStatus.getAddress() - } - } - - getYieldsBalancesUpdates(userWalletId, currenciesAddresses) - }, - flow4 = flowOf(value = false), - transform = ::createCurrenciesStatuses, - ) - .distinctUntilChanged() - } - .onEach { statusesLce -> - statusesLce.getOrNull()?.let { prevStatuses.value = it } - - send(statusesLce) - } - .launchIn(scope = this) - } - - private fun createCurrenciesStatuses( - currencies: NonEmptyList, - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeStakingBalances: Either>?, - isUpdating: Boolean, - ): Lce> = lce { - isLoading.set(isUpdating) - - val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val stakingBalances = maybeStakingBalances?.bindEither() - val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - null - } - - currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val stakingBalance = findStakingBalanceOrNull(stakingBalances, currency, networkStatus) - - val currencyStatus = CryptoCurrencyStatusFactory.create( - currency = currency, - maybeNetworkStatus = networkStatus.toOption(), - maybeQuoteStatus = quote.toOption(), - maybeStakingBalance = stakingBalance.toOption(), - ) - - currencyStatus - } - } - - private fun findStakingBalanceOrNull( - stakingBalances: List?, - currency: CryptoCurrency, - networkStatus: NetworkStatus?, - ): StakingBalance? { - if (stakingBalances.isNullOrEmpty()) return null - - val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value - val address = networkStatus.getAddress() - - return if (supportedIntegration != null && address != null) { - val stakingId = StakingID(integrationId = supportedIntegration, address = address) - - stakingBalances.firstOrNull { it.stakingId == stakingId } - ?: StakingBalance.Error(stakingId = stakingId) - } else { - null - } - } - - private fun getCurrencies(userWalletId: UserWalletId): EitherFlow> { - return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return getQuotesUpdates( - rawCurrencyIds = tokensIds.mapNotNullTo( - destination = hashSetOf(), - transform = CryptoCurrency.ID::rawCurrencyId, - ), - ) - } - - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return singleQuoteStatusSupplier( - params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), - ) - .map>> { setOf(it).right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - @OptIn(FlowPreview::class) - private fun getNetworkStatusesUpdates( - userWalletId: UserWalletId, - networks: NonEmptySet, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptySet()) - - networks.onEach { - launch { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = it), - ) - .onEach { status -> - state.update { loadedStatuses -> - loadedStatuses.addOrReplace(status) { it.network == status.network } - } - } - .launchIn(scope = this) - } - } - - state - .onEach(::send) - .launchIn(scope = this) - } - .debounce(timeoutMillis = 500) - .map, Either>> { it.right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - private fun getQuotesUpdates( - rawCurrencyIds: Set, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptySet()) - - rawCurrencyIds.onEach { - launch { - singleQuoteStatusSupplier( - params = SingleQuoteStatusProducer.Params(rawCurrencyId = it), - ) - .onEach { quote -> - state.update { loadedStatuses -> - loadedStatuses.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } - } - } - .launchIn(scope = this) - } - } - - state - .onEach(::send) - .launchIn(scope = this) - } - .map, Either>> { it.right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - private fun getYieldsBalancesUpdates( - userWalletId: UserWalletId, - cryptoCurrencies: Map, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptyList()) - - val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> - stakingIdFactory.create( - currencyId = currencyWithAddress.key, - defaultAddress = currencyWithAddress.value, - ) - .getOrNull() - } - - stakingIds.onEach { stakingId -> - launch { - singleStakingBalanceSupplier( - params = SingleStakingBalanceProducer.Params( - userWalletId = userWalletId, - stakingId = stakingId, - ), - ) - .onEach { balance -> - state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } - } - } - .launchIn(scope = this) - } - } - - state - .onEach { send(it.right()) } - .launchIn(scope = this) - } - .distinctUntilChanged() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt deleted file mode 100644 index e42588514f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.Either -import arrow.core.left -import arrow.core.raise.either -import arrow.core.right -import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.repository.CurrenciesRepository -import kotlinx.coroutines.flow.* - -@Suppress("LongParameterList") -internal class TokenListOperations( - private val currenciesRepository: CurrenciesRepository, - private val userWalletId: UserWalletId, - private val tokens: List, -) { - - fun getTokenListFlow(): Flow> { - return combine( - flow = getIsGrouped(), - flow2 = getIsSortedByBalance(), - ) { isGrouped, isSortedByBalance -> - either { - createTokenList(isGrouped = isGrouped.bind(), isSortedByBalance = isSortedByBalance.bind()) - } - } - } - - private fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { - val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty - - return TokenListFactory.create( - statuses = nonEmptyCurrencies, - groupType = if (isGrouped) TokensGroupType.NETWORK else TokensGroupType.NONE, - sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE, - ) - } - - private fun getIsGrouped(): Flow> { - return currenciesRepository.isTokensGrouped(userWalletId) - .map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(value = false.right()) } - .cancellable() - } - - private fun getIsSortedByBalance(): Flow> { - return currenciesRepository.isTokensSortedByBalance(userWalletId) - .map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(value = false.right()) } - .cancellable() - } - - sealed class Error { - - data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() - - data class DataError(val cause: Throwable) : Error() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 91a762bc11..c830032607 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -5,7 +5,6 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow @@ -16,58 +15,6 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface CurrenciesRepository { - /** - * Saves the given list of cryptocurrencies, along with the preferences for grouping and sorting, for a specific - * multi-currency user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The list of cryptocurrencies to be saved. - * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. - * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) - - /** - * Add currencies to a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The currencies which must be added. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun addCurrenciesCache(userWalletId: UserWalletId, currencies: List): List - - /** - * Removes currency from a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currency The currency which must be removed. - * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) - - /** - * Removes currencies from a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The currencies which must be removed. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) - /** * Retrieves the list of cryptocurrencies within a user wallet. * @@ -123,23 +70,6 @@ interface CurrenciesRepository { id: CryptoCurrency.ID, ): CryptoCurrency - /** - * Retrieves the list of cryptocurrencies within a multi-currency wallet. - * - * Loads cryptocurrencies if they have expired or if [refresh] is `true`. - * - * @param userWalletId The unique identifier of the user wallet. - * @param refresh A boolean flag indicating whether the data should be refreshed. - * @return A list of [CryptoCurrency]. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use MultiWalletCryptoCurrenciesSupplier") - suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean = false, - ): List - /** * Get the coin for a specific network. * @@ -153,28 +83,6 @@ interface CurrenciesRepository { derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin - /** - * Determines whether the tokens within a specific multi-currency user wallet are grouped. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use SingleAccountListSupplier instead") - fun isTokensGrouped(userWalletId: UserWalletId): Flow - - /** - * Determines whether the tokens within a specific multi-currency user wallet are sorted by balance. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use SingleAccountListSupplier instead") - fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow - /** * Determines whether the currency sending is blocked by network pending transaction * @@ -205,24 +113,8 @@ interface CurrenciesRepository { networkId: String, ): CryptoCurrency.Token - /** Get crypto currencies by [currencyRawId] from all user wallets */ - @Deprecated("Use MultiAccountListSupplier instead") - fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow>> - fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean - /** - * Synchronizes local tokens with remote data for a specific user wallet. - * This method ensures that the local token list matches the remote state by fetching - * the token data from the local cache and push it to backend. - * - * @param userWalletId The unique identifier of the user wallet to sync tokens for. - * @throws Exception if the sync request to the backend fails - */ - @Deprecated("Use AccountsCRUDRepository instead") - @Throws - suspend fun syncTokens(userWalletId: UserWalletId) - @Throws fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt index a30ede50c9..56c8bf8ef3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt @@ -9,4 +9,5 @@ enum class FetchingSource { NETWORK, QUOTE, STAKING, + TANGEM_PAY, } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 06b3f5f4ee..17b46927d6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -2,11 +2,13 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -45,6 +47,7 @@ class WalletBalanceFetcher internal constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -57,6 +60,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( @@ -72,6 +76,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -91,10 +96,18 @@ class WalletBalanceFetcher internal constructor( error("UserWallet doesn't contain crypto-currencies: $userWalletId") } - fetcher.fetch(userWalletId = userWalletId, currencies = currencies) + fetcher.fetch( + userWalletId = userWalletId, + currencies = currencies, + paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled, + ) } - private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set) { + private suspend fun BaseWalletBalanceFetcher.fetch( + userWalletId: UserWalletId, + currencies: Set, + paymentAccountRefactorEnabled: Boolean, + ) { coroutineScope { val results = fetchingSources.map { source -> async { @@ -102,6 +115,10 @@ class WalletBalanceFetcher internal constructor( FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies) FetchingSource.QUOTE -> fetchQuotes(currencies = currencies) FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies) + FetchingSource.TANGEM_PAY -> fetchPaymentAccount( + userWalletId = userWalletId, + paymentAccountRefactorEnabled = paymentAccountRefactorEnabled, + ) } source to maybeResult @@ -173,10 +190,19 @@ class WalletBalanceFetcher internal constructor( } } + private suspend fun fetchPaymentAccount( + userWalletId: UserWalletId, + paymentAccountRefactorEnabled: Boolean, + ): Either { + if (!paymentAccountRefactorEnabled) return Unit.right() + + return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } + /** * Params of [WalletBalanceFetcher] * * @property userWalletId user wallet id */ - data class Params(val userWalletId: UserWalletId) + data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index 8bd9d8a36c..eaa142f3fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -27,6 +27,7 @@ internal class MultiWalletBalanceFetcher( FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, ) override suspend fun getCryptoCurrencies(userWalletId: UserWalletId): Set { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt deleted file mode 100644 index be67de0762..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.error.DataError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.repository.MockCurrenciesRepository -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.mockk -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.random.Random - -internal class ApplyTokenListSortingUseCaseTest { - - private val userWalletId = UserWalletId(value = null) - - @Test - fun `when tokens are empty then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.TokenListIsEmpty.left() - - val useCase = getUseCase() - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when tokens saving failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val repository = getTokensRepository( - sortTokensResult = DataError.NetworkError.NoInternetConnection.left(), - ) - val useCase = getUseCase(repository) - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = MockTokens.tokens.map { it.id }.sortedByDescending { it.value }, - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = true - val expectedIsSorted = true - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = true - val expectedIsSorted = false - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = false - val expectedIsSorted = true - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = false - val expectedIsSorted = false - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.UnableToSortTokenList.left() - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = getSortedTokens().drop(n = 3).map { it.id }, - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - private fun getSortedTokens() = MockTokens.tokens - .sortedBy { Random.nextInt(0, MockTokens.tokens.size) } - - private fun getUseCase(tokensRepository: MockCurrenciesRepository = getTokensRepository()) = - ApplyTokenListSortingUseCase( - currenciesRepository = tokensRepository, - dispatchers = TestingCoroutineDispatcherProvider(), - multiWalletCryptoCurrenciesSupplier = mockk(), - ) - - private fun getTokensRepository( - sortTokensResult: Either = Unit.right(), - removeCurrencyResult: Either = Unit.right(), - tokens: Flow>> = flowOf(MockTokens.tokens.right()), - ): MockCurrenciesRepository { - return MockCurrenciesRepository( - sortTokensResult = sortTokensResult, - removeCurrencyResult = removeCurrencyResult, - token = MockTokens.token1.right(), - tokens = tokens, - isGrouped = emptyFlow(), - isSortedByBalance = emptyFlow(), - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt deleted file mode 100644 index 8630063505..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListGroupingTest { - - private val useCase = ToggleTokenListGroupingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt deleted file mode 100644 index 787d55393c..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListSortingUseCaseTest { - - private val useCase = ToggleTokenListSortingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt deleted file mode 100644 index befef5c116..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.domain.tokens.repository - -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.core.error.DataError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.FeePaidCurrency -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -internal class MockCurrenciesRepository( - private val sortTokensResult: Either, - private val removeCurrencyResult: Either, - private val token: Either, - private val tokens: Flow>>, - private val isGrouped: Flow>, - private val isSortedByBalance: Flow>, -) : CurrenciesRepository { - - var tokensIdsAfterSortingApply: List? = null - private set - - var isTokensGroupedAfterSortingApply: Boolean? = null - private set - - var isTokensSortedByBalanceAfterSortingApply: Boolean? = null - private set - - override suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) { - sortTokensResult.onLeft { throw it } - - tokensIdsAfterSortingApply = currencies - isTokensGroupedAfterSortingApply = isGroupedByNetwork - isTokensSortedByBalanceAfterSortingApply = isSortedByBalance - } - - override suspend fun addCurrenciesCache( - userWalletId: UserWalletId, - currencies: List, - ): List = emptyList() - - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { - removeCurrencyResult.onLeft { throw it } - } - - override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit - - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { - return emptyFlow() - } - - override suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean, - ): List { - return tokens.first().getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletPrimaryCurrency( - userWalletId: UserWalletId, - refresh: Boolean, - ): CryptoCurrency { - return token.getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletWithCardCurrencies( - userWalletId: UserWalletId, - refresh: Boolean, - ): List { - return tokens.first().getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletWithCardCurrency( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): CryptoCurrency { - return token.getOrElse { e -> throw e } - } - - override suspend fun getNetworkCoin( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { - TODO("Not yet implemented") - } - - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return isGrouped.map { it.getOrElse { e -> throw e } } - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return isSortedByBalance.map { it.getOrElse { e -> throw e } } - } - - override suspend fun isSendBlockedByPendingTransactions( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Boolean { - return false - } - - override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency { - return FeePaidCurrency.Coin - } - - override fun createCoinCurrency(network: Network): CryptoCurrency.Coin { - error("not implemented") - } - - override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { - return cryptoCurrency - } - - override suspend fun createTokenCurrency( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): CryptoCurrency.Token { - error("not implemented") - } - - override fun getAllWalletsCryptoCurrencies( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return emptyFlow() - } - - override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { - return false - } - - override suspend fun syncTokens(userWalletId: UserWalletId) { - return Unit - } - - override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { - error("No-op") - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index 2af6690bc8..937aa0d32d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID @@ -42,6 +43,7 @@ internal class WalletBalanceFetcherTest { private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( @@ -52,6 +54,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -76,7 +79,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -107,7 +115,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("Unknown type of wallet: $userWalletId").left() @@ -139,7 +152,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -171,7 +189,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns emptySet() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("UserWallet doesn't contain crypto-currencies: $userWalletId").left() @@ -213,7 +236,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -259,7 +287,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -311,7 +344,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -354,7 +392,12 @@ internal class WalletBalanceFetcherTest { } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -396,7 +439,12 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -444,7 +492,12 @@ internal class WalletBalanceFetcherTest { } returns stellarStakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -506,7 +559,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -572,7 +630,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -624,7 +687,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -674,7 +742,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt index 55bc9e7b8b..100c5964c1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt @@ -47,7 +47,12 @@ class MultiWalletBalanceFetcherTest { val actual = fetcher.fetchingSources // Assert - val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING) + val expected = setOf( + FetchingSource.NETWORK, + FetchingSource.QUOTE, + FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, + ) Truth.assertThat(actual).isEqualTo(expected) } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 559774d3e2..3c13ebe644 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,10 +24,6 @@ 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) /** Security */ implementation(deps.spongecastle.core) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt new file mode 100644 index 0000000000..bdc604087f --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +@Serializable +sealed class PaymentAccountStatus { + + abstract val source: StatusSource + + @Serializable + data object Loading : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object NotCreated : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + ) : PaymentAccountStatus() + + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Locked(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Loaded( + override val source: StatusSource, + val cardId: String, + val lastFourDigits: String, + val balance: SerializedBigDecimal, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + ) : PaymentAccountStatus() + + @Serializable + sealed class Error : PaymentAccountStatus() { + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class Unavailable(override val source: StatusSource) : Error() + + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object CardIssueFailed : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index 14edae3667..e2424a061a 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -9,7 +9,6 @@ data class TangemPayDetailsConfig( val cardId: String, val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, - val customerWalletAddress: String, val cardNumberEnd: String, val chainId: Int, ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt index 33289fa8f7..8f2196bef9 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt @@ -20,6 +20,7 @@ sealed class TangemPayTxHistoryItem { override val date: SerializedDateTime, override val amount: SerializedBigDecimal, override val currency: SerializedCurrency, + val authorizedAmount: SerializedBigDecimal, val localAmount: SerializedBigDecimal?, val localCurrency: SerializedCurrency?, val enrichedMerchantName: String?, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..740d9d0824 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.models.wallet.UserWalletId + +interface PaymentAccountStatusFetcher : FlowFetcher { + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..c49a25f45d --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus + +interface PaymentAccountStatusProducer : FlowProducer { + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt new file mode 100644 index 0000000000..94580d26e1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.pay.PaymentAccountStatus + +@Suppress("UnnecessaryAbstractClass") +abstract class PaymentAccountStatusSupplier( + override val factory: PaymentAccountStatusProducer.Factory, + override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 501df2011b..2fd02e7a45 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -1,6 +1,6 @@ package com.tangem.domain.pay.model -import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.models.kyc.KycStatus import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -22,31 +22,15 @@ data class CustomerInfo( val cardInfo: CardInfo?, ) { - enum class KycStatus { - /** Initial state */ - INIT, - - /** Performing the check */ - PENDING, - - /** SumSub approved */ - APPROVED, - - /** The check failed, documents rejected */ - REJECTED, - } - data class ProductInstance( val id: String, val cardId: String, - val cardFrozenState: TangemPayCardFrozenState, ) data class CardInfo( val lastFourDigits: String, val balance: BigDecimal, val currencyCode: String, - val customerWalletAddress: String, val depositAddress: String?, val isPinSet: Boolean, ) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt index 6ae706a0e1..327d8fd61f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -1,9 +1,9 @@ package com.tangem.domain.pay.model -enum class OrderStatus(val apiName: String) { - UNKNOWN(""), - NEW("NEW"), - PROCESSING("PROCESSING"), - COMPLETED("COMPLETED"), - CANCELED("CANCELED"), +enum class OrderStatus { + UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + NEW, + PROCESSING, + COMPLETED, + CANCELED, } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 568cf85006..52197ec817 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -23,7 +23,7 @@ interface OnboardingRepository { suspend fun getOrderId(userWalletId: UserWalletId): String? - suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either suspend fun checkCustomerEligibility(): Boolean suspend fun getCustomerEligibility(): Boolean 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 647e6ad412..4116e2e4f4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.pay.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* @@ -38,7 +39,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return // fast exit } - onboardingRepository.checkCustomerWallet(userWalletId) + onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") @@ -125,7 +126,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } .map { customerInfo -> Timber.tag(TAG).i("customerInfo") - if (customerInfo.cardInfo == null && customerInfo.kycStatus == CustomerInfo.KycStatus.APPROVED) { + if (customerInfo.cardInfo == null && customerInfo.kycStatus == KycStatus.APPROVED) { // If order id wasn't saved -> start order creation and get customer info onboardingRepository.createOrder(userWalletId) } @@ -151,7 +152,7 @@ class TangemPayMainScreenCustomerInfoUseCase( info = CustomerInfo( customerId = null, productInstance = null, - kycStatus = CustomerInfo.KycStatus.APPROVED, + kycStatus = KycStatus.APPROVED, cardInfo = null, ), orderStatus = orderData.status, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index e4b6af4d70..f7eb452cc2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -176,4 +176,29 @@ sealed class TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Button - Support On Transaction Popup", ) + + class KycPassedAndOrderCreated : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa KYC Passed And Order Created", + ) + + class KycRejected : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa KYC Rejected", + ) + + class KycCancelled : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa KYC Canceled", + ) + + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Banner Clicked", + ) + + class DetailsVisaPermanentButtonClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Button Clicked", + ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt index fb61bc2965..5b68240025 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt @@ -1,3 +1,3 @@ package com.tangem.domain.tangempay.model -data class TangemPayTxHistoryListConfig(val customerWalletAddress: String, val shouldRefresh: Boolean) \ No newline at end of file +data class TangemPayTxHistoryListConfig(val shouldRefresh: Boolean) \ 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 cf3f4c9f37..20ffd7cea6 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.CryptoPortfolio?, + val account: Account.CryptoPortfolio, val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt index e55e69f940..75c6fb808c 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -6,6 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet data class WcSessionApprove( val wallet: UserWallet, - val account: Account?, + val account: Account, val network: List, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index eaee191c92..21a22308f8 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId data class WcSessionDTO( val topic: String, val walletId: UserWalletId, - val accountId: AccountId? = null, + val accountId: AccountId = AccountId.forMainCryptoPortfolio(walletId), val url: String?, val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY, val connectingTime: Long? = null, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt index dbec25e6d2..9850e0418e 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -9,8 +9,7 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData data class WcSessionProposal( val dAppMetaData: WcAppMetaData, - val proposalNetwork: Map, - val proposalAccountNetwork: Map?, + val proposalAccountNetwork: Map, val securityStatus: CheckDAppResult, ) { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt new file mode 100644 index 0000000000..c370657bd3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt @@ -0,0 +1,242 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.common.util.getCardsCount +import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletIcon +import com.tangem.domain.models.wallet.isHotWallet +import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.runBlocking + +class GetWalletIconUseCase( + private val walletsRepository: WalletsRepository, +) { + + @Suppress("CyclomaticComplexMethod", "UnsafeCallOnNullableType") + operator fun invoke(userWallet: UserWallet): UserWalletIcon { + if (userWallet.isHotWallet) { + return UserWalletIcon.Hot + } + + userWallet.requireColdWallet() + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + val cardsCount = userWallet.getCardsCount() ?: 1 + + val cobrandColor by lazy { + colorByBatchId(userWallet.scanResponse.card.batchId) + } + + return when { + cardTypesResolver.isDevKit() -> otherColor(OtherCardType.Devkit) + userWallet.isRing() -> UserWalletIcon.Default(isRing = true, cardsCount = cardsCount) + cobrandColor != null -> cobrandColor!!.withCount(cardsCount) + cardTypesResolver.isWallet2() -> UserWalletIcon.Default(isRing = false, cardsCount = cardsCount) + cardTypesResolver.isShibaWallet() -> otherColor(OtherCardType.Shiba, cardsCount) + cardTypesResolver.isTangemWallet() -> otherColor(OtherCardType.Wallet1, cardsCount) + cardTypesResolver.isWhiteWallet() -> otherColor(OtherCardType.WhiteWallet, cardsCount) + cardTypesResolver.isTangemTwins() -> otherColor(OtherCardType.Twins, cardsCount) + cardTypesResolver.isStart2Coin() -> otherColor(OtherCardType.Starts2com, cardsCount) + cardTypesResolver.isTangemNote() -> + resolveNoteColor(userWallet) ?: UserWalletIcon.Stub(cardsCount = cardsCount) + DemoConfig.isDemoCardId(cardId = userWallet.cardId) -> + UserWalletIcon.Default(isRing = false, cardsCount = cardsCount) + else -> UserWalletIcon.Stub(cardsCount = cardsCount) + } + } + + private fun resolveNoteColor(userWallet: UserWallet.Cold): UserWalletIcon? { + val noteBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + + val otherCardType = when (noteBlockchain) { + Blockchain.Bitcoin -> OtherCardType.NoteBitcoin + Blockchain.Ethereum -> OtherCardType.NoteEthereum + Blockchain.XRP -> OtherCardType.NoteXRP + Blockchain.Binance -> OtherCardType.NoteBinance + Blockchain.Cardano -> OtherCardType.NoteCardano + Blockchain.Dogecoin -> OtherCardType.NoteDoge + else -> return null + } + + return otherColor(otherCardType) + } + + private fun otherColor(otherCardType: OtherCardType, cardsCount: Int = 1): UserWalletIcon.Colored { + return UserWalletIcon.Colored( + isRing = false, + mainColor = otherCardType.mainColor, + secondColor = if (cardsCount > 1) otherCardType.mainColor else null, + thirdColor = if (cardsCount > 2) otherCardType.mainColor else null, + ) + } + + private fun UserWalletIcon.Colored.withCount(count: Int): UserWalletIcon.Colored { + return this.copy( + mainColor = mainColor, + secondColor = if (count > 1) secondColor else null, + thirdColor = if (count > 2) thirdColor else null, + ) + } + + private fun UserWallet.Cold.isRing(): Boolean { + return scanResponse.cardTypesResolver.isRing() || + runBlocking { walletsRepository.isWalletWithRing(userWalletId = this@isRing.walletId) } + } + + @Suppress("CyclomaticComplexMethod") + private fun colorByBatchId(batchId: String): UserWalletIcon.Colored? { + fun color(main: String, second: String = main, third: String = main) = + UserWalletIcon.Colored(isRing = false, mainColor = main, secondColor = second, thirdColor = third) + + val cobrandType = CobrandType.entries.firstOrNull { it.batchIds.contains(batchId) } + + return when (cobrandType) { + CobrandType.Avrora -> color("#1E1E1C") + CobrandType.BabyDoge -> color("#E7D34C") + CobrandType.Bad -> color("#395467") + CobrandType.BitcoinGold -> color("#F08A1F") + CobrandType.BitcoinPizzaDay -> color("#AF4D37") + CobrandType.BitcoinPizza2 -> color("#F3C63A") + CobrandType.BTC365 -> color("#191E3E") + CobrandType.CashClubGold -> color("#D1BF78") + CobrandType.Changenow -> color("#191B2C") + CobrandType.Chilliz -> color("#49324A") + CobrandType.CoinMetrica -> color("#A140C7") + CobrandType.COQ -> color("#ED1F3A") + CobrandType.CryptoCasey -> color("#301E45") + CobrandType.CryptoOrg -> color("#27B39A") + CobrandType.CryptoSeth -> color("#414954") + CobrandType.GetsMine -> color("#AFC3CE") + CobrandType.Grim -> color("#131313") + CobrandType.Hodl -> color("#111111") + CobrandType.Jr -> color("#1C1C1C") + CobrandType.Kaspa -> color("#545C5C") + CobrandType.Kaspa2 -> color("#353535") + CobrandType.KaspaReseller -> color("#3B3D3A") + CobrandType.Kaspa3 -> color("#85CBC1") + CobrandType.Kasper -> color("#34302E") + CobrandType.Kaspy -> color("#DEC764") + CobrandType.Keiro -> color("#4D645C") + CobrandType.KishuInu -> color("#2DA4CE") + CobrandType.Kango -> color("#5BA495") + CobrandType.Konan -> color("#64C9C9") + CobrandType.Kroak -> color("#8EB8AF") + CobrandType.Neiro -> color("#E7A524") + CobrandType.NewWorldElite -> color("#292722") + CobrandType.PassimPay -> color("#6A4361") + CobrandType.Pastel -> color("#FFC7A4", "#84A479", "#6FB5BB") + CobrandType.Pepecoin -> color("#303439") + CobrandType.RamenCat -> color("#DEBE88") + CobrandType.RedPanda -> color("#C5C5C5") + CobrandType.Rizo -> color("#1765A7") + CobrandType.Sakura -> color("#F0E9C4") + CobrandType.SatoshiFriends -> color("#242424") + CobrandType.SinCity -> color("#D3C487") + CobrandType.SpringBloom -> color("#FAC73A") + CobrandType.StealthCard -> color("#555557") + CobrandType.SunDrop -> color("#FEC035") + CobrandType.Trillant -> color("#955091") + CobrandType.Tron -> color("#D4221D") + CobrandType.Upbit -> color("#2B2B2B") + CobrandType.USA -> color("#0D185F") + CobrandType.VeChain -> color("#5186A2") + CobrandType.Vivid -> color("#D3CF09", "#F76952", "#2BCAD1") + CobrandType.Vnish -> color("#292522") + CobrandType.VoltInu -> color("#34312B") + CobrandType.WhiteTangem -> color("#D3D3D3") + CobrandType.WildGoat -> color("#1B1B1B") + CobrandType.Winter -> color("#7FB9C8", "#80BDE8", "#B2C6E4") + CobrandType.WinterSakura -> color("#88B9E9") + CobrandType.LockedMoney -> color("#272625") + CobrandType.Ghoad -> color("#6EC5C5") + CobrandType.BlushSky -> color("#C1E9E8", "#FACAD3", "#DCCEE0") + CobrandType.ElectraSea -> color("#0D5A67", "#29939E", "#30C6B1") + CobrandType.HyperBlue -> color("#0F397C", "#1474D3", "#0BC9EC") + CobrandType.Lunar -> color("#B0313A") + null -> null + } + } +} + +private enum class CobrandType(val batchIds: List) { + Avrora(listOf("AF18")), + BabyDoge(listOf("AF51")), + Bad(listOf("AF09")), + BitcoinGold(listOf("AF71", "AF990016", "AF990009")), + BitcoinPizzaDay(listOf("AF33")), + BitcoinPizza2(listOf("AF990019")), + BTC365(listOf("AF97")), + CashClubGold(listOf("BB000004")), + Changenow(listOf("BB000013")), + Chilliz(listOf("BB000016")), + CoinMetrica(listOf("AF27")), + COQ(listOf("AF28")), + CryptoCasey(listOf("AF21", "AF22", "AF23")), + CryptoOrg(listOf("AF57")), + CryptoSeth(listOf("AF32")), + GetsMine(listOf("BB000008")), + Grim(listOf("AF13")), + Hodl(listOf("BB000009")), + Jr(listOf("AF14")), + Kaspa(listOf("AF08")), + Kaspa2(listOf("AF25", "AF61", "AF72")), + KaspaReseller(listOf("AF31")), + Kaspa3(listOf("AF73")), + Kasper(listOf("AF96")), + Kaspy(listOf("AF95")), + Keiro(listOf("BB000017")), + KishuInu(listOf("AF52")), + Kango(listOf("BB000006")), + Konan(listOf("AF93")), + Kroak(listOf("BB000011")), + Neiro(listOf("AF98")), + NewWorldElite(listOf("AF26")), + PassimPay(listOf("BB000007")), + Pastel(listOf("AF43", "AF44", "AF45", "AF78", "AF79", "AF80")), + Pepecoin(listOf("BB000015")), + RamenCat(listOf("AF990006", "AF990007", "AF990008")), + RedPanda(listOf("AF34")), + Rizo(listOf("BB000012")), + Sakura(listOf("AF990029", "AF990030", "AF990031", "AF990071", "AF990072", "AF990073")), + SatoshiFriends(listOf("AF19")), + SinCity(listOf("BB000010")), + SpringBloom(listOf("AF990001", "AF990002", "AF990004")), + StealthCard(listOf("AF60", "AF74", "AF88")), + SunDrop(listOf("AF990005", "AF990003")), + Trillant(listOf("AF16")), + Tron(listOf("AF07")), + Upbit(listOf("BB000019")), + USA(listOf("AF91", "AF990017", "AF990056")), + VeChain(listOf("AF29")), + Vivid(listOf("AF40", "AF41", "AF42", "AF75", "AF76", "AF77")), + Vnish(listOf("BB000005")), + VoltInu(listOf("AF35")), + WhiteTangem(listOf("AF15")), + WildGoat(listOf("BB000001")), + Winter(listOf("AF85", "AF86", "AF87", "AF990013", "AF990012", "AF990011")), + WinterSakura(listOf("AF990053", "AF990054", "AF990055")), + LockedMoney(listOf("AF63")), + Ghoad(listOf("AF89")), + BlushSky(listOf("AF990020", "AF990021", "AF990022")), + ElectraSea(listOf("AF990023", "AF990024", "AF990025")), + HyperBlue(listOf("AF990026", "AF990027", "AF990028", "AF990050", "AF990051", "AF990052")), + Lunar(listOf("AF990057", "AF990058", "AF990059")), +} + +private enum class OtherCardType(val mainColor: String) { + NoteXRP("#726799"), + NoteDoge("#BFB565"), + NoteEthereum("#989C9F"), + NoteBinance("#C9B87C"), + NoteCardano("#5979AD"), + NoteBitcoin("#EABE8B"), + Starts2com("#356F99"), + Wallet1("#2E3944"), + Twins("#B8B7B6"), + Devkit("#938C92"), + WhiteWallet("#DDDDDD"), + Shiba("#D5963A"), +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt index b18a7db430..2a24783240 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent { ) : Params data class Edit( - val account: Account, + val account: Account.CryptoPortfolio, ) : Params } } \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt index d1c47280ea..99450a8474 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt @@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account interface AccountDetailsComponent : ComposableContentComponent { interface Factory : ComponentFactory - data class Params(val account: Account) + data class Params(val account: Account.CryptoPortfolio) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index d100998aca..5ef205ef80 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.account.createedit import androidx.annotation.StringRes +import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toDomain @@ -25,7 +26,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents @@ -73,7 +73,7 @@ internal class AccountCreateEditModel @Inject constructor( when (params) { is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId) is AccountCreateEditComponent.Params.Edit -> { - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex) analyticsEventHandler.send(event) } @@ -135,10 +135,14 @@ internal class AccountCreateEditModel @Inject constructor( result .onLeft { error -> handleAddAccountError(error, derivationIndex.value) } - .onRight { + .onRight { account -> analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated()) showMessage(R.string.account_create_success_message) - router.pop() + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.ACCOUNT, + accountId = account.accountId, + ) + router.replaceCurrent(route) } } @@ -170,7 +174,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon) val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex)) uiState.value = uiState.value.toggleProgress(showProgress = true) @@ -182,7 +186,7 @@ internal class AccountCreateEditModel @Inject constructor( uiState.value = uiState.value.toggleProgress(showProgress = false) result - .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) } + .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) } .onRight { showMessage(R.string.account_edit_success_message) router.pop() diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index 47b06df25c..d9b4788e38 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -14,12 +14,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase 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.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent @@ -54,29 +51,29 @@ internal class AccountDetailsModel @Inject constructor( init { analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened()) - singleAccountSupplier(SingleAccountProducer.Params(accountId)) + singleAccountSupplier.filterCryptoPortfolioAccount(accountId) .onEach { account -> uiState.update { buildUI(account) } } .launchIn(modelScope) } - private fun onEditAccountClick(account: Account) { + private fun onEditAccountClick(account: Account.CryptoPortfolio) { analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit()) router.push(AppRoute.EditAccount(account)) } - private fun onManageTokensClick(account: Account) { + private fun onManageTokensClick(account: Account.CryptoPortfolio) { val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, - portfolioId = PortfolioId(account.accountId), + accountId = account.accountId, ) analyticsEventHandler.send( - AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value), + AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value), ) router.push(route) } private fun onArchiveAccountClick() { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation) analyticsEventHandler.send(event) confirmArchiveDialog() @@ -86,7 +83,7 @@ internal class AccountDetailsModel @Inject constructor( val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), onClick = { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation) analyticsEventHandler.send(event) }, @@ -107,7 +104,7 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation) analyticsEventHandler.send(event) uiState.update { it.toggleProgress(true) } @@ -128,7 +125,7 @@ internal class AccountDetailsModel @Inject constructor( val event = AccountSettingsAnalyticEvents.AccountError( source = AccountSettingsAnalyticEvents.Source.ARCHIVE, error = error.tag, - accountDerivation = params.account.derivationIndex?.value, + accountDerivation = params.account.derivationIndex.value, ) analyticsEventHandler.send(event) val titleRes: Int @@ -155,16 +152,13 @@ internal class AccountDetailsModel @Inject constructor( messageSender.send(dialogMessage) } - private fun buildUI(account: Account): AccountDetailsUM { - val archiveMode = when (account) { - is Account.CryptoPortfolio -> when (account.isMainAccount) { - true -> ArchiveMode.None - false -> ArchiveMode.Available( - onArchiveAccountClick = ::onArchiveAccountClick, - isLoading = false, - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM { + val archiveMode = when (account.isMainAccount) { + true -> ArchiveMode.None + false -> ArchiveMode.Available( + onArchiveAccountClick = ::onArchiveAccountClick, + isLoading = false, + ) } val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull() ?.isMultiCurrency == true diff --git a/features/approval/api/build.gradle.kts b/features/approval/api/build.gradle.kts new file mode 100644 index 0000000000..0401b93110 --- /dev/null +++ b/features/approval/api/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + + /** Common */ + implementation(projects.common.ui) + + /** Other */ + implementation(deps.kotlin.immutable.collections) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt new file mode 100644 index 0000000000..e2345bbc2d --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.approval.api + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId + +interface GiveApprovalComponent : ComposableBottomSheetComponent { + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val amount: String, + val spenderAddress: String, + val subtitle: TextReference, + val callback: Callback, + ) + + interface Callback { + fun onApproveClick() + fun onApproveDone() + fun onApproveFailed() + fun onCancelClick() + } + + interface Factory { + fun create(context: AppComponentContext, params: Params): GiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..46410d3fbf --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.approval.api + +interface GiveApprovalFeatureToggles { + + val isGaslessApprovalEnabled: Boolean +} \ No newline at end of file diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts new file mode 100644 index 0000000000..e15b2662cd --- /dev/null +++ b/features/approval/impl/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.impl" +} + +dependencies { + + /** Feature */ + implementation(projects.features.approval.api) + implementation(projects.features.sendV2.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + + /** Common */ + implementation(projects.common.ui) + + /** SDK */ + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.timber) + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt new file mode 100644 index 0000000000..c0102d54a6 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -0,0 +1,104 @@ +package com.tangem.features.approval.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.impl.model.GiveApprovalModel +import com.tangem.features.approval.impl.ui.GiveApprovalContent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import com.tangem.common.ui.R as CommonUiR + +internal class DefaultGiveApprovalComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: GiveApprovalComponent.Params, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : GiveApprovalComponent, AppComponentContext by appComponentContext { + + private val model: GiveApprovalModel = getOrCreateModel(params = params) + + private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = child("giveApprovalFeeSelector"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = FeeSelectorUM.Loading, + onLoadFee = { model.loadFee() }, + onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) }, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY, + analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve, + userWalletId = params.userWalletId, + ), + onResult = model::onFeeResult, + ) + + private val currency: String = params.cryptoCurrencyStatus.currency.symbol + + override fun dismiss() { + params.callback.onCancelClick() + } + + @Composable + override fun BottomSheet() { + val uiState by model.uiState.collectAsStateWithLifecycle() + + val config = remember { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.secondary, + titleText = resourceReference(CommonUiR.string.give_permission_title), + titleAction = TopAppBarButtonUM.Icon( + iconRes = CommonUiR.drawable.ic_information_24, + onClicked = model::showPermissionInfoDialog, + ), + ) { + GiveApprovalContent( + currency = currency, + subtitle = params.subtitle, + approveType = uiState.approveType, + approveItems = uiState.approveItems, + onChangeApproveType = model::onChangeApproveType, + walletInteractionIcon = uiState.walletInteractionIcon, + isApproveEnabled = uiState.isApproveButtonEnabled, + isApproveLoading = uiState.isApproveLoading, + onApproveClick = model::onApproveClick, + onCancelClick = model::onCancelClick, + onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + } + } + + @AssistedFactory + interface Factory : GiveApprovalComponent.Factory { + override fun create( + context: AppComponentContext, + params: GiveApprovalComponent.Params, + ): DefaultGiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..b135009ed6 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt @@ -0,0 +1,14 @@ +package com.tangem.features.approval.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.approval.api.GiveApprovalFeatureToggles +import javax.inject.Inject + +internal class DefaultGiveApprovalFeatureToggles @Inject constructor( + private val featureToggles: FeatureTogglesManager, +) : GiveApprovalFeatureToggles { + + // Remove GiveTxPermissionBottomSheet and all dependencies with this toggle + override val isGaslessApprovalEnabled: Boolean + get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED") +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt new file mode 100644 index 0000000000..3f6fdce55a --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -0,0 +1,39 @@ +package com.tangem.features.approval.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles +import com.tangem.features.approval.impl.DefaultGiveApprovalComponent +import com.tangem.features.approval.impl.DefaultGiveApprovalFeatureToggles +import com.tangem.features.approval.impl.model.GiveApprovalModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal interface GiveApprovalFeatureModule { + + @Singleton + @Binds + fun bindGiveApprovalFeatureToggle(toggles: DefaultGiveApprovalFeatureToggles): GiveApprovalFeatureToggles + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface GiveApprovalModelModule { + + @Binds + @IntoMap + @ClassKey(GiveApprovalModel::class) + fun bindModel(model: GiveApprovalModel): Model +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt new file mode 100644 index 0000000000..87882d24ce --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -0,0 +1,243 @@ +package com.tangem.features.approval.impl.model + +import androidx.compose.runtime.Stable +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class GiveApprovalModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase, + private val getFeeForTokenUseCase: GetFeeForTokenUseCase, + private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, + private val uiMessageSender: UiMessageSender, + private val urlOpener: UrlOpener, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), FeeSelectorModelCallback { + + private val params: GiveApprovalComponent.Params = paramsContainer.require() + + private val userWallet by lazy { + requireNotNull( + getUserWalletUseCase(params.userWalletId).getOrNull(), + ) { "No wallet found for id: $params.userWalletId" } + } + + val uiState: StateFlow + field = MutableStateFlow( + GiveApprovalUM( + approveType = ApproveType.LIMITED, + walletInteractionIcon = walletInterationIcon(userWallet), + isApproveButtonEnabled = false, + isApproveLoading = false, + ), + ) + + private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + this.feeSelectorUM = feeSelectorUM + uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) } + } + + fun onApproveClick() { + params.callback.onApproveClick() + uiState.update { it.copy(isApproveLoading = true) } + modelScope.launch(dispatchers.main) { + val isSuccess = sendApprovalTransaction() + uiState.update { it.copy(isApproveLoading = false) } + if (isSuccess) { + params.callback.onApproveDone() + } else { + params.callback.onApproveFailed() + } + } + } + + fun onCancelClick() { + params.callback.onCancelClick() + } + + fun onChangeApproveType(approveType: ApproveType) { + uiState.update { it.copy(approveType = approveType) } + } + + fun onOpenLearnMoreAboutApproveClick() { + urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP) + } + + fun showPermissionInfoDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer), + title = resourceReference(com.tangem.common.ui.R.string.common_approve), + ), + ) + } + + suspend fun prepareApprovalTransaction(): Either { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: return Either.Left(IllegalStateException("Currency is not a token")) + + return createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWalletId, + amount = getApprovalAmount(), + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ) + } + + suspend fun loadFee(): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return getFeeUseCase( + transactionData = approvalTransaction, + userWallet = userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } + + suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return if (maybeToken == null) { + getFeeForGaslessUseCase( + transactionData = approvalTransaction, + userWallet = userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } else { + getFeeForTokenUseCase( + transactionData = approvalTransaction, + userWallet = userWallet, + token = maybeToken.currency, + ) + } + } + + private suspend fun sendApprovalTransaction(): Boolean { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false + + val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false + val selectedFee = feeContent.selectedFeeItem.fee + val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended + + val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency + + val transactionData = createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWalletId, + amount = getApprovalAmount(), + fee = selectedFee, + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + Timber.e(error, "Failed to create approval transaction") + return false + } + + return if (isFeeInTokenCurrency) { + createAndSendGaslessTransactionUseCase( + userWallet = userWallet, + transactionData = transactionData, + fee = feeExtended, + ) + } else { + sendTransactionUseCase( + txData = transactionData, + userWallet = userWallet, + network = tokenCurrency.network, + ) + }.fold( + ifLeft = { error -> + Timber.e("Failed to send approval transaction: $error") + false + }, + ifRight = { + sendApproveSuccessAnalytics(feeContent) + true + }, + ) + } + + private fun sendApproveSuccessAnalytics(feeContent: FeeSelectorUM.Content) { + val currency = params.cryptoCurrencyStatus.currency + val feeToken = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol + val permissionType = when (uiState.value.approveType) { + ApproveType.LIMITED -> "Current transaction" + ApproveType.UNLIMITED -> "Unlimited" + } + val event = AnalyticsParam.TxSentFrom.Approve( + blockchain = currency.network.name, + token = currency.symbol, + feeType = feeContent.toAnalyticType(), + feeToken = feeToken, + permissionType = permissionType, + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = event, + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + } + + private fun getApprovalAmount(): BigDecimal? { + return if (uiState.value.approveType == ApproveType.LIMITED) { + params.amount.toBigDecimalOrNull() + } else { + null + } + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt new file mode 100644 index 0000000000..96fc768b88 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.approval.impl.model + +import androidx.annotation.DrawableRes +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal data class GiveApprovalUM( + val approveType: ApproveType, + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), + @DrawableRes val walletInteractionIcon: Int?, + val isApproveButtonEnabled: Boolean, + val isApproveLoading: Boolean, +) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt new file mode 100644 index 0000000000..2d3386f430 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -0,0 +1,353 @@ +package com.tangem.features.approval.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +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.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.PopupProperties +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import com.tangem.common.ui.R as CommonUiR + +@Composable +@Suppress("LongParameterList") +internal fun GiveApprovalContent( + currency: String, + subtitle: TextReference, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + walletInteractionIcon: Int?, + isApproveEnabled: Boolean, + isApproveLoading: Boolean, + onApproveClick: () -> Unit, + onCancelClick: () -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24), + ) + + SpacerH16() + + ApprovalInfo( + currency = currency, + approveType = approveType, + approveItems = approveItems, + onChangeApproveType = onChangeApproveType, + onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButtonIconEnd( + text = stringResourceSafe(id = CommonUiR.string.common_approve), + iconResId = walletInteractionIcon, + showProgress = isApproveLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onApproveClick, + enabled = isApproveEnabled, + ) + + SpacerH12() + + SecondaryButton( + text = stringResourceSafe(id = CommonUiR.string.common_cancel), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onCancelClick, + ) + + SpacerH16() + } +} + +@Suppress("LongParameterList") +@Composable +private fun ApprovalInfo( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, +) { + FooterContainer( + footer = annotatedReference { + append(stringResourceSafe(CommonUiR.string.swap_approve_description)) + append(" ") + withLink( + link = LinkAnnotation.Clickable( + tag = "APPROVE_TAG", + linkInteractionListener = { onOpenLearnMoreAboutApproveClick() }, + ), + block = { + appendColored( + text = stringResourceSafe(CommonUiR.string.common_learn_more), + color = TangemTheme.colors.text.accent, + ) + }, + ) + }, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + AmountItem( + currency = currency, + approveType = approveType, + onChangeApproveType = onChangeApproveType, + approveItems = approveItems, + ) + } + SpacerH16() + FooterContainer( + footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { mutableStateOf(false) } + var amountSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + SpacerWMax() + Text( + text = approveType.text.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { type -> + isExpandSelector = false + onChangeApproveType(type) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + properties = PopupProperties(clippingEnabled = false), + offset = with(LocalDensity.current) { + DpOffset(x = offsetX.toDp(), y = offsetY.toDp()) + }, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.action) + .onSizeChanged { dropDownWidth = it }, + ) { + items.forEach { item -> + val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent + + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + text = { + Row { + Text( + text = when (item) { + ApproveType.LIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_unlimited, + ) + }, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + SpacerWMax() + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24), + ), + tint = color, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.size20), + ) + } + }, + onClick = { + onItemClick.invoke(item) + }, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun GiveApprovalContentPreview( + @PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams, +) { + TangemThemePreview { + GiveApprovalContent( + currency = params.currency, + subtitle = params.subtitle, + approveType = params.approveType, + approveItems = params.approveItems, + onChangeApproveType = {}, + walletInteractionIcon = params.walletInteractionIcon, + isApproveEnabled = params.isApproveEnabled, + isApproveLoading = params.isApproveLoading, + onApproveClick = {}, + onCancelClick = {}, + onOpenLearnMoreAboutApproveClick = {}, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + ) + } +} + +private data class GiveApprovalPreviewParams( + val currency: String, + val subtitle: TextReference, + val approveType: ApproveType, + val approveItems: ImmutableList, + val walletInteractionIcon: Int?, + val isApproveEnabled: Boolean, + val isApproveLoading: Boolean, +) + +private class GiveApprovalContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + GiveApprovalPreviewParams( + currency = "USDT", + subtitle = stringReference("Allow this app to access your USDT"), + approveType = ApproveType.LIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = true, + isApproveLoading = false, + ), + GiveApprovalPreviewParams( + currency = "USDC", + subtitle = stringReference("Allow this app to access your USDC"), + approveType = ApproveType.UNLIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = false, + isApproveLoading = true, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..a08d796627 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.approval.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM + +internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + override fun updateState(feeSelectorUM: FeeSelectorUM) { + } + + @Composable + override fun Content(modifier: Modifier) { + } +} \ 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 05292329ad..1cf9bc5fc0 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 @@ -23,6 +23,7 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -150,6 +151,7 @@ internal class DetailsModel @Inject constructor( } } + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Settings)) sendFeedbackEmailUseCase(feedbackType) } } @@ -230,6 +232,7 @@ internal class DetailsModel @Inject constructor( } } + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Settings)) sendFeedbackEmailUseCase(feedbackType) } } @@ -262,6 +265,7 @@ internal class DetailsModel @Inject constructor( private fun onTangemPayItemClicked() { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.DetailsVisaPermanentButtonClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt index 72d5656bad..1513ec7932 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt @@ -1,6 +1,5 @@ package com.tangem.features.feed.entry.featuretoggle interface FeedFeatureToggle { - val isFeedEnabled: Boolean val isEarnBlockEnabled: Boolean } \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index beb8db8937..df759d55a1 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) @@ -56,12 +57,6 @@ dependencies { implementation(projects.domain.yieldSupply) implementation(projects.domain.earn) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) 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 02b1c82ab7..31b71bb3ea 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 @@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent -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 @@ -20,7 +19,6 @@ import javax.inject.Inject internal class FeedEntryChildFactory @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory, ) { @@ -66,7 +64,6 @@ internal class FeedEntryChildFactory @Inject constructor( appComponentContext = appComponentContext, params = child.params, analyticsEventHandler = analyticsEventHandler, - accountsFeatureToggles = accountsFeatureToggles, portfolioComponentFactory = portfolioComponentFactory, ) } 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 c3fc430041..d43ace2233 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 @@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency @@ -33,7 +32,6 @@ internal class DefaultMarketsTokenDetailsComponent( appComponentContext: AppComponentContext, val params: Params, analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { @@ -117,7 +115,6 @@ internal class DefaultMarketsTokenDetailsComponent( modifier = modifier, backgroundColor = LocalMainBottomSheetColor.current.value, state = state, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, portfolioBlock = portfolioComponent?.let { component -> { blockModifier -> component.Content(blockModifier) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 6df78dda91..a1cf255e9b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -58,7 +58,6 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( bottomSheet.child?.instance?.BottomSheet() } - @Suppress("UnsafeCallOnNullableType") private fun bottomSheetChild( config: MarketsPortfolioRoute, componentContext: ComponentContext, @@ -66,7 +65,7 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create( context = childByContext(componentContext), params = AddToPortfolioComponent.Params( - addToPortfolioManager = model.newAddToPortfolioManager!!, + addToPortfolioManager = model.addToPortfolioManager, callback = model.addToPortfolioCallback, ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt deleted file mode 100644 index c22e143ea1..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.loader - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Loader of portfolio data - * - * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses - * @property getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings - * @property getWalletTotalBalanceUseCase use case for getting wallet total balance - * -[REDACTED_AUTHOR] - */ -internal class PortfolioDataLoader @Inject constructor( - private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) { - - /** Load data by [currencyRawId] */ - @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: CryptoCurrency.RawID): Flow { - return combine( - flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> - PortfolioData( - walletsWithCurrencies = walletsWithCurrencies, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - walletsWithBalance = emptyMap(), - ) - } - // setup balances for wallets from walletsWithCurrencyStatuses - .flatMapLatest { portfolioData -> - getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), - ) - .map { portfolioData.copy(walletsWithBalance = it) } - .onEmpty { emit(portfolioData) } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) - .distinctUntilChanged() - .map { walletsWithMaybeStatuses -> - walletsWithMaybeStatuses.mapValues { entry -> - entry.value.mapNotNull { it.getOrNull() } - } - } - .flatMapLatest { walletsWithStatuses -> - val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> - statuses.map { status -> - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { - YieldSupplyAvailability.Unavailable - } - getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) - .map { tokenActionsState -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = tokenActionsState.states, - ) - } - } - } - - combine(actionsFlows) { actions -> - walletsWithStatuses.mapValues { entry -> - entry.value.mapNotNull { status -> - actions.firstOrNull { - it.userWallet == entry.key && it.status == status - } - } - } - }.onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { status -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = emptyList(), - ) - } - }, - ) - } - }.onEmpty { - emit(emptyMap()) - } - .distinctUntilChanged() - } - - private fun getWalletsWithTotalBalanceFlow( - ids: List, - ): Flow>> { - return combine( - flows = ids - .map { userWalletId -> - getWalletTotalBalanceUseCase(userWalletId) - .map { userWalletId to it } - .distinctUntilChanged() - }, - transform = { it.toMap() }, - ) - .distinctUntilChanged() - .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } - } -} \ 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/AddToPortfolioBSContentUMFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt deleted file mode 100644 index e139659522..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.toImmutableList - -/** - * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] - * - * @property token token params - * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed - * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed - * @property onNetworkSwitchClick callback is invoked when network switch is clicked - * @property onAnotherWalletSelect callback is invoked when wallet is selected - * @property onContinueClick callback is invoked when continue button is clicked - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class AddToPortfolioBSContentUMFactory( - private val addToPortfolioManager: AddToPortfolioManager, - private val token: TokenMarketParams, - private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, - private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, - private val onAnotherWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, -) { - - /** - * Create [TangemBottomSheetConfig] - * - - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param alreadyAddedNetworks already added networks - */ - @Suppress("LongParameterList") - fun create( - currentState: TangemBottomSheetConfig?, - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - selectedWallet: UserWallet?, - alreadyAddedNetworks: Set?, - artworks: Map, - ): TangemBottomSheetConfig { - return (currentState ?: TangemBottomSheetConfig.Empty).copy( - isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible, - onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, - content = if (selectedWallet != null && alreadyAddedNetworks != null) { - AddToPortfolioBSContentUM( - selectedWallet = selectedWallet.toSelectedUserWalletItemUM( - portfolioData = portfolioData, - balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(), - artwork = artworks[selectedWallet.walletId], - ), - selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = addToPortfolioManager.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - addToPortfolioData = portfolioUIData.addToPortfolioData, - ), - alreadyAddedNetworks = alreadyAddedNetworks, - onNetworkSwitchClick = onNetworkSwitchClick, - ).convert(value = token), - isScanCardNotificationVisible = portfolioUIData.isNeededColdWalletInteraction, - isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks( - userWalletId = selectedWallet.walletId, - ), - onContinueButtonClick = { - onContinueClick( - selectedWallet.walletId, - portfolioUIData.addToPortfolioData.getAddedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - ) - }, - walletSelectorConfig = createWalletSelectorBSConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible, - portfolioData = portfolioData, - selectedWalletId = selectedWallet.walletId, - artworks = artworks, - ), - isWalletBlockVisible = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency).size > 1, - ) - } else { - TangemBottomSheetConfigContent.Empty - }, - ) - } - - private fun UserWallet.toSelectedUserWalletItemUM( - artwork: UserWalletItemUM.ImageState? = null, - portfolioData: PortfolioData, - balance: TotalFiatBalance?, - ): UserWalletItemUM { - return UserWalletItemUMConverter( - onClick = { onWalletSelectorVisibilityChange(true) }, - endIcon = UserWalletItemUM.EndIcon.Arrow, - balance = balance, - artwork = artwork, - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - ).convert(value = this) - } - - private fun createWalletSelectorBSConfig( - isShow: Boolean, - portfolioData: PortfolioData, - selectedWalletId: UserWalletId, - artworks: Map, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShow, - onDismissRequest = { onWalletSelectorVisibilityChange(false) }, - content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency) - .map { it.key } - .map { userWallet -> - val balance = portfolioData.walletsWithBalance[userWallet.walletId] - - UserWalletItemUMConverter( - onClick = { id -> - if (id != selectedWalletId) { - onAnotherWalletSelect(id) - onWalletSelectorVisibilityChange(false) - } - }, - appCurrency = portfolioData.appCurrency, - balance = balance?.getOrNull(), - isBalanceHidden = portfolioData.isBalanceHidden, - endIcon = if (userWallet.walletId == selectedWalletId) { - UserWalletItemUM.EndIcon.Checkmark - } else { - UserWalletItemUM.EndIcon.None - }, - artwork = artworks[userWallet.walletId], - ).convert(userWallet) - } - .toImmutableList(), - onBack = { onWalletSelectorVisibilityChange(false) }, - ), - ) - } -} \ 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/AddToPortfolioManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt deleted file mode 100644 index d176536657..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import timber.log.Timber -import javax.inject.Inject -import kotlin.collections.firstOrNull -import kotlin.collections.orEmpty -import kotlin.collections.set - -internal typealias WalletsWithNetworks = Map> - -/** - * Manager for tracking changing networks in AddToPortfolio - * -[REDACTED_AUTHOR] - */ -internal class AddToPortfolioManager @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, -) { - - val availableNetworks = MutableStateFlow?>(value = null) - private val addedNetworks = MutableStateFlow(value = emptyMap()) - private val removedNetworks = MutableStateFlow(value = emptyMap()) - - /** Get [AddToPortfolioData] as flow */ - fun getAddToPortfolioData(): Flow { - return combine( - flow = availableNetworks, - flow2 = addedNetworks, - flow3 = removedNetworks, - transform = ::AddToPortfolioData, - ) - } - - /** Set available networks [networks] */ - fun setAvailableNetworks(networks: List) { - availableNetworks.value = networks.toSet() - } - - /** Add network [networkId] to [userWalletId] */ - fun addNetwork(userWalletId: UserWalletId, networkId: String) { - addedNetworks.add(userWalletId, networkId) - - removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) - } - - /** Remove network [networkId] from [userWalletId] */ - fun removeNetwork(userWalletId: UserWalletId, networkId: String) { - removedNetworks.add(userWalletId, networkId) - - addedNetworks.cancelPrevChangeIfExist( - userWalletId = userWalletId, - networkId = networkId, - ) - } - - /** Remove all networks by [userWalletId] */ - fun removeAllChanges(userWalletId: UserWalletId) { - addedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - - removedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - } - - fun associateWithToggle( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - addToPortfolioData: AddToPortfolioData, - ): Map { - val filteredNetworks = filterAvailableNetworksForWalletUseCase( - userWalletId = userWalletId, - networks = addToPortfolioData.availableNetworks.orEmpty(), - ) - // Use user choice or check already added networks - return filteredNetworks.associateWith { availableNetwork -> - val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork) - - if (isAddedByUser == true) return@associateWith true - - val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork) - - if (isRemovedByUser == true) return@associateWith false - - val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } - - isAddedBefore - } - } - - private fun MutableStateFlow.cancelPrevChangeIfExist( - userWalletId: UserWalletId, - networkId: String, - ) { - if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) - } - - private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) - } - - private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) - } - - private fun MutableStateFlow.change( - userWalletId: UserWalletId, - networkId: String, - isAddAction: Boolean, - ) { - val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } - - if (network == null) { - Timber.d( - "Network [$networkId] doesn't contain in available networks [%s]", - availableNetworks.value?.joinToString { it.networkId }, - ) - - return - } - - update { walletsWithNetworks -> - walletsWithNetworks.toMutableMap().apply { - this[userWalletId] = if (isAddAction) { - this[userWalletId].orEmpty() + network - } else { - this[userWalletId].orEmpty() - network - } - } - } - } - - /** - * Add to portfolio data - * - * @property availableNetworks available networks that user can add to portfolio - * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet - * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet - * - * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just - * toggle it. But when we will save user changes, we will check what tokens have already been added or - * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] - */ - data class AddToPortfolioData( - val availableNetworks: Set?, - val addedNetworks: WalletsWithNetworks, - val removedNetworks: WalletsWithNetworks, - ) { - - fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() || - removedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ - fun getAddedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() - - return addedNetworksByUser.map { it.networkId } - .minus(alreadyAddedNetworkIds) - .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - - /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ - fun getRemovedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() - - return alreadyAddedNetworkIds - .minus(removedNetworksByUser.map { it.networkId }.toSet()) - .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - } -} \ 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/MarketsPortfolioDelegate.kt similarity index 97% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index 2df91aab47..50cf8f9881 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/MarketsPortfolioDelegate.kt @@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network 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.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.account.models.AccountStatusList @@ -45,7 +44,7 @@ import kotlinx.coroutines.flow.* @OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") -internal class NewMarketsPortfolioDelegate @AssistedInject constructor( +internal class MarketsPortfolioDelegate @AssistedInject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val allAccountSupplier: MultiAccountStatusListSupplier, @@ -106,10 +105,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( private fun addFirstTokenFlow(): Flow = buttonState.map { state -> when (state) { MyPortfolioUM.Tokens.AddButtonState.Loading -> MyPortfolioUM.Loading - MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken( - onAddClick = onAddClick, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - ) + MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) MyPortfolioUM.Tokens.AddButtonState.Unavailable -> MyPortfolioUM.Unavailable } } @@ -314,7 +310,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( tokenActionsHandler: TokenActionsHandler, buttonState: Flow, onAddClick: () -> Unit, - ): NewMarketsPortfolioDelegate + ): MarketsPortfolioDelegate } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt index 71d48270b1..0401beb1e7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,91 +5,56 @@ 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.ui.userwallet.state.UserWalletItemUM 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.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase 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.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.feed.impl.R -import com.tangem.features.wallet.utils.UserWalletImageFetcher -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager as NewAddToPortfolioManager -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") @Stable @ModelScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val messageSender: UiMessageSender, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val portfolioDataLoader: PortfolioDataLoader, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val addToPortfolioManager: AddToPortfolioManager, - private val analyticsEventHandler: AnalyticsEventHandler, - private val userWalletImageFetcher: UserWalletImageFetcher, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val marketsPortfolioDelegateFactory: MarketsPortfolioDelegate.Factory, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val tokenActionsHandlerFactory: TokenActionsHandler.Factory, private val receiveAddressesFactory: ReceiveAddressesFactory, - accountsFeatureToggles: AccountsFeatureToggles, - newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory, - newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, + override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - val state: StateFlow get() = _state + val state: StateFlow + field = MutableStateFlow(value = MyPortfolioUM.Loading) private val params = paramsContainer.require() + private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( tokenSymbol = params.token.symbol, source = params.analyticsParams?.source, ) - val newAddToPortfolioManager: NewAddToPortfolioManager? - val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate? + private val currentAppCurrency = createAppCurrencyFlow() + private val tokenActionsHandler = createTokenActionsHandler() - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ - private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + val addToPortfolioManager: AddToPortfolioManager = createAddToPortfolioManager() + private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate() val bottomSheetNavigation: SlotNavigation = SlotNavigation() val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { @@ -97,321 +62,78 @@ internal class MarketsPortfolioModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss() } - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler = tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> - analyticsEventHandler.send( - analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = handledAction - .cryptoCurrencyData - .status - .currency - .network - .name, - ), - ) - configureReceiveAddresses(handledAction) - }, - ) - - private val factory = MyPortfolioUMFactory( - onAddClick = { - onAddToPortfolioBSVisibilityChange(isShow = true) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioClicked(), - ) - }, - addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( - addToPortfolioManager = addToPortfolioManager, - token = params.token, - onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, - onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, - onNetworkSwitchClick = ::onNetworkSwitchClick, - onAnotherWalletSelect = { walletId -> - onWalletSelect(walletId) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioWalletChanged(), - ) - }, - onContinueClick = { selectedWalletId, addedNetworks -> - onContinueClick(selectedWalletId, addedNetworks) - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioContinue( - blockchainNames = addedNetworks.mapNotNull { - BlockchainUtils.getNetworkInfo(it.networkId)?.name - }, - ), - ) - }, - ), - currentState = Provider { _state.value }, - tokenActionsHandler = tokenActionsHandler, - updateTokens = { updateBlock -> - updateTokensState { state -> - state.copy(tokens = updateBlock(state.tokens)) - } - }, - ) - init { - if (accountsFeatureToggles.isFeatureEnabled) { - newAddToPortfolioManager = newAddToPortfolioManagerFactory - .create( - modelScope, - params.token, - params.analyticsParams?.source?.let { NewAddToPortfolioManager.AnalyticsParams(it) }, - ) - newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create( - scope = modelScope, - token = params.token, - tokenActionsHandler = tokenActionsHandler, - buttonState = newAddToPortfolioManager.state.map { state -> - when (state) { - is NewAddToPortfolioManager.State.AvailableToAdd -> { - MyPortfolioUM.Tokens.AddButtonState.Available - } - NewAddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading - NewAddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable - } - }, - onAddClick = { - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) - bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) - }, - ) - newMarketsPortfolioDelegate.combineData() - .onEach { _state.value = it } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - newAddToPortfolioManager = null - newMarketsPortfolioDelegate = null - // Subscribe on selected wallet flow to support actual selected wallet - subscribeOnSelectedMultiWalletUpdates() - - subscribeOnStateUpdates() - } + marketsPortfolioDelegate.combineData() + .onEach { state.value = it } + .flowOn(dispatchers.default) + .launchIn(modelScope) } fun setTokenNetworks(networks: List) { - addToPortfolioManager.setAvailableNetworks(networks) - newAddToPortfolioManager?.setTokenNetworks(networks) - newMarketsPortfolioDelegate?.setTokenNetworks(networks) + addToPortfolioManager.setTokenNetworks(networks) + marketsPortfolioDelegate.setTokenNetworks(networks) } fun setNoNetworksAvailable() { - addToPortfolioManager.setAvailableNetworks(emptyList()) - newAddToPortfolioManager?.setTokenNetworks(emptyList()) - newMarketsPortfolioDelegate?.setTokenNetworks(emptyList()) + addToPortfolioManager.setTokenNetworks(emptyList()) + marketsPortfolioDelegate.setTokenNetworks(emptyList()) } - private fun subscribeOnSelectedMultiWalletUpdates() { - getSelectedWalletUseCase() - .getOrElse { e -> - Timber.e("Failed to load selected wallet: $e") - error("Failed to load selected wallet") - } - .onEach { userWallet -> - selectedMultiWalletIdFlow.value = userWallet.takeIf { it.isMultiCurrency }?.walletId - } - .launchIn(modelScope) - } - - private fun subscribeOnStateUpdates() { - combine( - flow = loadPortfolioDataWithArtworks(params.token.id), - flow2 = getPortfolioUIDataFlow(), - transform = { pair, portfolioUIData -> - val (portfolioData, artworks) = pair - factory.create(portfolioData, portfolioUIData, artworks) - }, - ) - .onEach { _state.value = it } - .launchIn(modelScope) - } - - private fun loadPortfolioDataWithArtworks( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - val wallets = Channel>() - val portfolioFlow = portfolioDataLoader - .load(currencyRawId) - .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - - val artworksFlow = wallets.receiveAsFlow() - .distinctUntilChanged() - .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } - - return combine( - flow = portfolioFlow, - flow2 = artworksFlow, - ) { portfolioData, artworks -> portfolioData to artworks } - } - - private fun getPortfolioUIDataFlow(): Flow { - return combine( - flow = portfolioBSVisibilityModelFlow, - flow2 = selectedMultiWalletIdFlow, - flow3 = addToPortfolioManager.getAddToPortfolioData(), - transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> - PortfolioUIData( - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, - isNeededColdWalletInteraction = isNeededColdWalletInteraction(selectedWalletId, addToPortfolioData), - ) - }, + private fun createAddToPortfolioManager(): AddToPortfolioManager { + return addToPortfolioManagerFactory.create( + scope = modelScope, + token = params.token, + analyticsParams = params.analyticsParams?.source?.let { AddToPortfolioManager.AnalyticsParams(it) }, ) } - private suspend fun isNeededColdWalletInteraction( - selectedWalletId: UserWalletId?, - addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - ): Boolean { - return if (selectedWalletId != null) { - coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedWalletId, - networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() - .associate { it.networkId to null }, - ) - } else { - false - } - } - - private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { - val selectedWalletId = selectedMultiWalletIdFlow.value - - if (selectedWalletId == null) { - Timber.e("Impossible to switch network when selected wallet is null") - return - } - - if (isChecked) { - modelScope.launch { - val unsupportedState = checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = blockchainRowUM.id, - isMainNetwork = blockchainRowUM.isMainNetwork, - ) - if (unsupportedState != null) { - showUnsupportedWarning(unsupportedState) - } else { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) + private fun createMarketsPortfolioDelegate(): MarketsPortfolioDelegate { + return marketsPortfolioDelegateFactory.create( + scope = modelScope, + token = params.token, + tokenActionsHandler = tokenActionsHandler, + buttonState = addToPortfolioManager.state.map { state -> + when (state) { + is AddToPortfolioManager.State.AvailableToAdd -> { + MyPortfolioUM.Tokens.AddButtonState.Available + } + AddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading + AddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable } - } - } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - - private suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { throwable -> - Timber.e( - throwable, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = throwable.localizedMessage - ?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) + }, + onAddClick = { + analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) + bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) }, ) - - messageSender.send(message) } - private fun onWalletSelect(userWalletId: UserWalletId) { - selectedMultiWalletIdFlow.update { prevUserWalletId -> - prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) - - userWalletId - } - } - - private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { - modelScope.launch { - saveMarketTokensUseCase( - userWalletId = userWalletId, - tokenMarketParams = params.token, - addedNetworks = addedNetworks, - removedNetworks = emptySet(), + private fun createAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, ) - - onAddToPortfolioBSVisibilityChange(isShow = false) - - addToPortfolioManager.removeAllChanges(userWalletId) - } } - private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false) - } - } - - private fun onWalletSelectorVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow) - } - } - - private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { - _state.update { stateToUpdate -> - val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate - block(tokensState) - } + private fun createTokenActionsHandler(): TokenActionsHandler { + return tokenActionsHandlerFactory.create( + currentAppCurrency = Provider { currentAppCurrency.value }, + onHandleQuickAction = { handledAction -> + val currency = handledAction.cryptoCurrencyData.status.currency + analyticsEventHandler.send( + analyticsEventBuilder.quickActionClick( + actionUM = handledAction.action, + blockchainName = currency.network.name, + ), + ) + configureReceiveAddresses(handledAction) + }, + ) } private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt deleted file mode 100644 index 72ff24db98..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList - -/** - * Factory for creating [MyPortfolioUM] - * - * @property onAddClick callback when user wants to add new token - * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content - * @property tokenActionsHandler token actions handler - * @property currentState current state provider - * @property updateTokens callback for updating tokens - * -[REDACTED_AUTHOR] - */ -internal class MyPortfolioUMFactory( - private val onAddClick: () -> Unit, - private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, - private val tokenActionsHandler: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) { - - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): MyPortfolioUM { - val addToPortfolioData = portfolioUIData.addToPortfolioData - - val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true - if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { - portfolioData.walletsWithCurrencies - } else { - portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) - } - - val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() - if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() - - return if (hasMultiWallets) { - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - ) - } else { - MyPortfolioUM.UnavailableForWallet - } - } - - return TokensPortfolioUMConverter( - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - addButtonState = walletsWithCurrencies.getAddButtonState( - availableNetworks = addToPortfolioData.availableNetworks, - ), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - quickActionsIntents = tokenActionsHandler, - currentState = currentState, - updateTokens = updateTokens, - ) - .convert(walletsWithCurrencies) - } - - private fun createAddToPortfolioBSConfig( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): TangemBottomSheetConfig { - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } - - val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() - - val alreadyAddedNetworks = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet] - ?.filter { !it.status.currency.isCustom } - ?.map { it.status.currency.network.backendId } - ?.toSet() - - return addToPortfolioBSContentUMFactory.create( - currentState = currentState().addToPortfolioBSConfig, - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - selectedWallet = selectedWallet, - alreadyAddedNetworks = alreadyAddedNetworks, - artworks = artworks, - ) - } - - private fun Map>.getAddButtonState( - availableNetworks: Set?, - ): MyPortfolioUM.Tokens.AddButtonState { - if (availableNetworks == null) return MyPortfolioUM.Tokens.AddButtonState.Loading - - val networkIds = availableNetworks.map { it.networkId } - - val isAllAvailableNetworksAdded = this - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (isAllAvailableNetworksAdded) { - MyPortfolioUM.Tokens.AddButtonState.Unavailable - } else { - MyPortfolioUM.Tokens.AddButtonState.Available - } - } - - /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( - networks: Set, - ): Map> { - return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } - } - - /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( - networks: Set, - ): List { - val networkIds = networks.map(TokenMarketInfo.Network::networkId) - - return mapNotNull { cryptoCurrencyData -> - cryptoCurrencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) } - } - } -} \ 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/PortfolioBSVisibilityModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt deleted file mode 100644 index dd3f13b3e7..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -/** - * Model for portfolio bottom sheet visibility - * - * @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet - * @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioBSVisibilityModel( - val isAddToPortfolioBSVisible: Boolean = false, - val isWalletSelectorBSVisible: Boolean = false, -) \ 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/PortfolioUIData.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt deleted file mode 100644 index 894b86dc98..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Portfolio UI data. Combined data from all UI flows that required to setup portfolio - * - * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @property selectedWalletId selected wallet id - * @property addToPortfolioData add to portfolio data - * @property isNeededColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioUIData( - val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - val selectedWalletId: UserWalletId?, - val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - val isNeededColdWalletInteraction: Boolean, -) \ 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/SelectNetworkUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt deleted file mode 100644 index 5f9c6b282e..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [TokenMarketParams] to [SelectNetworkUM] - * - * @property networksWithToggle map of networks with toggles - * @property alreadyAddedNetworks already added networks - * @property onNetworkSwitchClick callback is called when network switch is clicked - * -[REDACTED_AUTHOR] - */ -internal class SelectNetworkUMConverter( - private val networksWithToggle: Map, - private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketParams): SelectNetworkUM { - return SelectNetworkUM( - tokenId = value.id.value, - iconUrl = value.imageUrl, - tokenName = value.name, - tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter(alreadyAddedNetworks) - .convertList(networksWithToggle.toList()) - .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, - ) - } -} \ 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/TokenActionsHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt index 7bfe5544bd..3f6071b417 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt @@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage @@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor( private val router: Router, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val currentAppCurrency: Provider, @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt deleted file mode 100644 index 249415e21f..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class TokensPortfolioUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val addButtonState: MyPortfolioUM.Tokens.AddButtonState, - private val bsConfig: TangemBottomSheetConfig, - private val onAddClick: () -> Unit, - private val quickActionsIntents: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { - - override fun convert(value: Map>): MyPortfolioUM.Tokens { - val currentTokensState = currentState() as? MyPortfolioUM.Tokens - - return MyPortfolioUM.Tokens( - tokens = value - .flatMap { entry -> entry.value } - .map { cryptoData -> - PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { toggleQuickActions(cryptoData) }, - tokenActionsHandler = quickActionsIntents, - ).convert(value = cryptoData) to cryptoData - } - .setQuickActionsVisibility(currentState = currentTokensState) - .toImmutableList(), - buttonState = addButtonState, - addToPortfolioBSConfig = bsConfig, - onAddClick = onAddClick, - ) - } - - private fun List>.setQuickActionsVisibility( - currentState: MyPortfolioUM.Tokens?, - ): List { - return when { - // if there is only one token and it has empty balance, show quick actions for it - currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = true) - } - } - // if there is no previous state, hide quick actions for all tokens - currentState == null -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = false) - } - } - else -> { - val previousList = currentState.tokens - - // otherwise, keep previous state - this.map { (token, _) -> - token.copy( - isQuickActionsShown = previousList - .firstOrNull { it.matchWith(token) } - ?.isQuickActionsShown == true, - ) - } - } - } - } - - private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return cryptoData.status.value.amount?.isZero() == true - } - - private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { - updateTokens { tokenList -> - tokenList.map { portfolioTokenUM -> - portfolioTokenUM.copy( - isQuickActionsShown = if (portfolioTokenUM.matchWith(cryptoData)) { - !portfolioTokenUM.isQuickActionsShown - } else { - false - }, - ) - }.toImmutableList() - } - } - - private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { - return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id - } - - private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return this.walletId == cryptoData.userWallet.walletId && - this.tokenItemState.id == cryptoData.status.currency.id.value - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt deleted file mode 100644 index 16993ea52d..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ /dev/null @@ -1,383 +0,0 @@ -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.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.rows.ArrowRow -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -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.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.features.feed.impl.R -import kotlinx.coroutines.delay - -@Composable -internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - titleText = resourceReference(R.string.common_add_to_portfolio), - ) { contentUM -> - Content( - modifier = Modifier.fillMaxWidth(), - state = contentUM, - ) - - WalletSelectorBottomSheet(contentUM.walletSelectorConfig) - } -} - -@Composable -private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - var continueButtonAreaHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val scrollState = rememberScrollState() - - Box(modifier = modifier) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.isWalletBlockVisible) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - SpacerH12() - } - - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - SpacerH12() - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) - SpacerH12() - } - - // Scroll to the bottom when the notification appears and the scroll is at the bottom - LaunchedEffect(Unit) { - if (scrollState.canScrollForward.not()) { - delay(timeMillis = 500) - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - - SpacerH(with(density) { continueButtonAreaHeight.toDp() }) - } - - AnimatedVisibility( - visible = scrollState.canScrollForward, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter), - ) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - - ContinueButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - continueButtonAreaHeight = it.size.height - }, - enabled = state.isContinueButtonEnabled, - isTangemIconVisible = state.isScanCardNotificationVisible, - onClick = state.onContinueButtonClick, - ) - } -} - -@Composable -private fun ContinueButton( - enabled: Boolean, - isTangemIconVisible: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TangemButton( - enabled = enabled, - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ) - .navigationBarsPadding() - .fillMaxWidth(), - text = stringResourceSafe(R.string.common_continue), - icon = if (enabled && isTangemIconVisible) { - TangemButtonIconPosition.End(R.drawable.ic_tangem_24) - } else { - TangemButtonIconPosition.None - }, - showProgress = false, - size = TangemButtonSize.Default, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - onClick = onClick, - animateContentChange = true, - ) -} - -@Suppress("LongMethod") -@Composable -private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(R.string.markets_select_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing14), - verticalAlignment = Alignment.CenterVertically, - ) { - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - SpacerW12() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = false) - .alignByBaseline(), - text = state.tokenName, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW6() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .alignByBaseline(), - text = state.tokenCurrencySymbol, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Visible, - maxLines = 1, - ) - } - - state.networks.fastForEachIndexed { index, network -> - ArrowRow( - isLastItem = index == state.networks.lastIndex, - content = { - BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - model = network, - action = { - TangemSwitch( - checked = network.isSelected, - checkedColor = if (network.isEnabled) { - TangemTheme.colors.control.checked - } else { - TangemTheme.colors.icon.inactive - }, - onCheckedChange = { checked -> - if (checked) { - hapticManager.perform(TangemHapticEffect.View.ToggleOn) - } else { - hapticManager.perform(TangemHapticEffect.View.ToggleOff) - } - - state.onNetworkSwitchClick(network, checked) - }, - enabled = network.isEnabled, - ) - }, - ) - }, - ) - } - } - } -} - -@Composable -private fun ScanWalletWarning(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background( - color = TangemTheme.colors.button.disabled, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.markets_generate_addresses_notification), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - content = content, - onDismissRequest = {}, - ), - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContent( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContentRtl( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview(rtl = true) { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -// For on device testing -@Composable -@Preview -private fun PreviewContentTestOnDevice( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview( - alwaysShowBottomSheets = false, - ) { - var isShow by remember { mutableStateOf(false) } - - var contentState by remember { - mutableStateOf(content) - } - - LaunchedEffect(Unit) { - contentState = content.copy( - onContinueButtonClick = { - contentState = contentState.copy( - isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, - ) - }, - isContinueButtonEnabled = true, - selectedWallet = content.selectedWallet.copy( - onClick = { - contentState = contentState.copy( - isContinueButtonEnabled = !contentState.isContinueButtonEnabled, - ) - }, - ), - ) - } - - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = isShow, - content = contentState, - onDismissRequest = { isShow = false }, - ), - ) - - Button( - onClick = { isShow = !isShow }, - ) { - Text(text = "Toggle") - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt index 8d6911c448..c7c885eca6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt @@ -70,11 +70,6 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { is MyPortfolioUM.Content -> PortfolioList(state = state) } } - - val bsConfig = state.addToPortfolioBSConfig - if (bsConfig != null) { - AddToPortfolioBottomSheet(config = bsConfig) - } } @Composable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt deleted file mode 100644 index 7b4a266b9e..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -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.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - title = { content -> - TangemBottomSheetTitle(content.title) - }, - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: TokenActionsBSContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - content.actions.forEachIndexed { index, action -> - Box( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.actions.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - ) { - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconRes, - redesign = true, - onItemsClick = { content.onActionClick(action) }, - ) - } - } - } -} - -@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)) { - TokenActionsBottomSheet( - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TokenActionsBSContentUM( - title = "Wallet 1", - actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), - onActionClick = {}, - ), - ), - ) - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt deleted file mode 100644 index 530786cf30..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -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.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM -import com.tangem.features.feed.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - title = { content -> - TangemTopAppBar( - title = resourceReference(R.string.common_choose_wallet), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(content.onBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - }, - ) { content -> - Content( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - state = content, - ) - } -} - -@Composable -private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Column( - modifier = modifier - .verticalScroll(rememberScrollState()), - ) { - BlockCard( - modifier = Modifier.fillMaxSize(), - colors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( - modifier = Modifier.fillMaxWidth(), - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - state = state, - ) - } - } - } - SpacerH(bottomBarHeight) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - WalletSelectorBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewContent() { - TangemThemePreview { - Content( - state = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt deleted file mode 100644 index 4de442ca27..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.features.feed.impl.R -import kotlinx.collections.immutable.persistentListOf - -internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = "1", - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ) - - val userWallet = UserWalletItemUM( - id = "1", - name = stringReference("Wallet 1"), - information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), - balance = UserWalletItemUM.Balance.Loading, - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ), - blockchainRow, - blockchainRow, - ), - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = true, - isContinueButtonEnabled = true, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium Etherium Etherium Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ).copy(name = "Etherium Etherium Etherium Etherium"), - *Array(25) { blockchainRow }, - ), - - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = false, - isContinueButtonEnabled = false, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - ) -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 8c6bf7a7a1..f84d983885 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -3,7 +3,6 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.pre import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState @@ -22,25 +21,19 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Unit, - val walletSelectorConfig: TangemBottomSheetConfig, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt index d567f3095f..b9d4593c30 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -1,16 +1,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class MyPortfolioUM { - abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? - data class Tokens( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, val tokens: ImmutableList, val buttonState: AddButtonState, val onAddClick: () -> Unit, @@ -27,25 +23,15 @@ internal sealed class MyPortfolioUM { val items: ImmutableList, val buttonState: Tokens.AddButtonState, val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty - } + ) : MyPortfolioUM() data class AddFirstToken( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() - data object Loading : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object Loading : MyPortfolioUM() - data object Unavailable : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object Unavailable : MyPortfolioUM() - data object UnavailableForWallet : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object UnavailableForWallet : MyPortfolioUM() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt deleted file mode 100644 index 4ee09913a4..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -internal data class SelectNetworkUM( - val tokenId: String, - val iconUrl: String?, - val tokenName: String, - val tokenCurrencySymbol: String, - val networks: ImmutableList, - val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt deleted file mode 100644 index e93a65ddcd..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletSelectorBSContentUM( - val userWallets: ImmutableList, - val onBack: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt index 38b3458572..3e15b106b0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt @@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle( private val featureTogglesManager: FeatureTogglesManager, ) : FeedFeatureToggle { - override val isFeedEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED") - override val isEarnBlockEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED") } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt index 245f6b84c7..204c185109 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt @@ -1,12 +1,12 @@ package com.tangem.features.feed.model.converter -import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.news.ShortArticle +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.features.feed.ui.utils.mapFormattedDate import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -16,7 +16,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet internal class ShortArticleToArticleConfigUMConverter( - private val isTrending: Provider, + private val isTrending: Provider?, ) : Converter, ImmutableList> { override fun convert(value: List): ImmutableList { @@ -25,7 +25,7 @@ internal class ShortArticleToArticleConfigUMConverter( id = shortArticle.id, title = shortArticle.title, score = shortArticle.score, - isTrending = isTrending(), + isTrending = isTrending?.invoke() ?: shortArticle.isTrending, tags = buildArticleTags(shortArticle), createdAt = mapFormattedDate(shortArticle.createdAt), isViewed = shortArticle.viewed, 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 bb91b6a704..8622c63f4b 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 @@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv 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.state.transformers.* import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM @@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor( private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) private val earnListConfigProvider = Provider { createEarnTokensListConfig( - selectedTypeFilter = stateController.value.selectedTypeFilter, - selectedNetworkFilter = stateController.value.selectedNetworkFilter, + selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter, + selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter, earnNetworks = earnNetworks.value, ) } @@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor( init { updateInitialState() fetchEarnNetworks() - fetchTopEarnTokens() subscribeOnStoredFilters() subscribeOnNetworks() subscribeOnBatchFlow() @@ -117,19 +114,22 @@ internal class EarnModel @Inject constructor( batchFlowManager.initialLoadingError, batchFlowManager.paginationStatus, ) { items, error, paginationStatus -> - val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || - state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks - error?.let(::handleBestOpportunitiesErrorAnalytics) + val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks EarnListStateManager.calculateState( items = items, error = error, paginationStatus = paginationStatus, hasActiveFilters = hasActiveFilters, - onRetryClick = { batchFlowManager.reload() }, + onRetryClick = { + batchFlowManager.reload() + reloadEarnNetworks() + }, onLoadMore = { batchFlowManager.loadMore() }, onClearFiltersClick = ::onClearFiltersClick, - ) - }.onEach { bestOpportunitiesState -> + ) to error + }.onEach { (bestOpportunitiesState, error) -> + error?.let(::handleBestOpportunitiesErrorAnalytics) stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) }.launchIn(modelScope) } @@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor( private fun subscribeOnStoredFilters() { modelScope.launch(dispatchers.default) { - getEarnFilterUseCase() - .collect { filter -> - val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) - val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) - stateController.update( - EarnFilterSelectedStateTransformer( - filterType = typeFilterUM, - filterNetwork = networkFilterUM, - ), - ) - batchFlowManager.reload() - } + combine( + getEarnFilterUseCase(), + earnNetworks, + ) { filter, networks -> + val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) + val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + stateController.update( + EarnFilterSelectedStateTransformer( + filterType = typeFilterUM, + filterNetwork = networkFilterUM, + earnNetworks = networks, + ), + ) + batchFlowManager.reload() + }.collect() } } private fun fetchTopEarnTokens() { modelScope.launch(dispatchers.default) { + stateController.update(UpdateMostlyUsedStateLoadingTransformer()) fetchTopEarnTokensUseCase() } } @@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor( } } + private fun reloadEarnNetworks() { + modelScope.launch(dispatchers.default) { + if (earnNetworks.value.isLeft()) { + fetchEarnNetworks() + } + } + } + /* start of clicks area */ private fun onTypeFilterClick() { val currentState = state.value bottomSheetNavigation.activate( FeedBottomSheetRoute.TypeFilter( params = EarnTypeFilterComponent.Params( - selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter), + selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter), onFilterSelected = ::onTypeFilterOptionSelected, onDismiss = { bottomSheetNavigation.dismiss() }, ), @@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor( } private fun createNetworkFilters(): List { - val selectedFilter = state.value.selectedNetworkFilter + val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter return buildList { add( EarnFilterNetwork.AllNetworks( @@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor( modelScope.launch(dispatchers.default) { setEarnFilterUseCase( EarnFilter( - earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter), + earnFilterNetwork = EarnFilterNetworkUMConverter().convert( + value = state.value.earnFilterUM.selectedNetworkFilter, + ), earnFilterType = type, ), ) bottomSheetNavigation.dismiss() + reloadEarnNetworks() } } @@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor( setEarnFilterUseCase( EarnFilter( earnFilterNetwork = filter, - earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter), + earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter), ), ) } @@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor( is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty() else -> null to "" } - analyticsEventHandler.send( - EarnAnalyticsEvent.BestOpportunitiesLoadError( - code = code, - message = message, - ), - ) + if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) { + 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 index 08845ff277..68a5a99bfe 100644 --- 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 @@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig( earnNetworks.fold( ifLeft = { null }, ifRight = { networks -> - networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + networks.filter(EarnNetwork::isAdded) + .map(EarnNetwork::networkId) + .ifEmpty { listOf(NO_ONE_NETWORK) } }, ) } @@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig( networks = networks, isForEarn = isForEarn, ) -} \ No newline at end of file +} + +/** + * This id means that backend has to return empty result + */ +private const val NO_ONE_NETWORK = "-1" \ 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 index c608bc31c7..3ff24b4467 100644 --- 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 @@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent( event = "Best Opportunities Filter Network Applied", params = mapOf( "Network Filter Type" to filterType.value, - "NetworkId" to networkId.orEmpty(), + "Network Id" to networkId.orEmpty(), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 6cec5cd393..f074cbdc66 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() { return EarnUM( mostlyUsed = EarnListUM.Loading, bestOpportunities = EarnBestOpportunitiesUM.Loading, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, 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 index 9bdb205b49..5e8c4ef1c5 100644 --- 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 @@ -1,18 +1,24 @@ package com.tangem.features.feed.model.earn.state.transformers +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 import com.tangem.features.feed.ui.earn.state.EarnUM internal class EarnFilterSelectedStateTransformer( + private val earnNetworks: EarnNetworks, private val filterType: EarnFilterTypeUM, private val filterNetwork: EarnFilterNetworkUM, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { return prevState.copy( - selectedTypeFilter = filterType, - selectedNetworkFilter = filterNetwork, + earnFilterUM = prevState.earnFilterUM.copy( + selectedTypeFilter = filterType, + selectedNetworkFilter = filterNetwork, + isNetworkFilterEnabled = earnNetworks.isRight { networks -> networks.isNotEmpty() }, + isTypeFilterEnabled = true, + ), ) } } \ 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 deleted file mode 100644 index a4ae7833a3..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt +++ /dev/null @@ -1,13 +0,0 @@ -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/UpdateMostlyUsedStateLoadingTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt new file mode 100644 index 0000000000..e1fcb79a35 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(mostlyUsed = EarnListUM.Loading) + } +} \ 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 fde61b8825..bd7ea3d8c3 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 @@ -271,7 +271,7 @@ internal class FeedComponentModel @Inject constructor( earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { EarnListUM.Loading } else { - null + EarnListUM.Empty }, ) } 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 99440c5949..7100cff366 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 @@ -70,7 +70,7 @@ internal class FeedStateController @Inject constructor( earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { EarnListUM.Loading } else { - null + EarnListUM.Empty }, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt index fd81cc6428..d95b950076 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -47,7 +47,7 @@ internal class UpdateEarnStateTransformer( } private fun handleEmptyState(currentState: FeedListUM): FeedListUM { - return currentState.copy(earnListUM = null) + return currentState.copy(earnListUM = EarnListUM.Empty) } private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM { @@ -69,6 +69,8 @@ internal class UpdateEarnStateTransformer( currentState: FeedListUM, earnTokensWithCurrency: List, ): FeedListUM { + if (earnTokensWithCurrency.isEmpty()) return currentState.copy(earnListUM = EarnListUM.Empty) + val newItems = earnTokensWithCurrency .sortedWith( compareByDescending { 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 e4af70344b..819d362a4a 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 @@ -9,6 +9,8 @@ import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.sorted import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -163,6 +165,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( }, onGeneratedAINotificationClick = { modelScope.launch { + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Market)) sendFeedbackEmailUseCase( type = FeedbackEmailType.CurrencyDescriptionError( currencyId = params.token.id.value, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt index ee57aec32a..a966da27f2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt @@ -40,6 +40,7 @@ internal class NewsDetailsConverter( newsUrl = value.newsUrl, relatedTokens = value.relatedTokens.toImmutableList(), isLiked = value.isLiked, + isTrending = value.isTrending, ) } 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 6b9886c03f..ca5f6d7382 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 @@ -1,12 +1,12 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.news.model.NewsListConfig import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter import com.tangem.features.feed.model.converter.distinctBatchesContent +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction import com.tangem.pagination.PaginationStatus @@ -30,7 +30,7 @@ internal open class NewsListBatchFlowManager( ) { private val actionsFlow = MutableSharedFlow>() private val converter by lazy { - ShortArticleToArticleConfigUMConverter(isTrending = Provider { false }) + ShortArticleToArticleConfigUMConverter(null) } private val batchFlow = getNewsListBatchFlowUseCase( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt index e16a20e9f2..9c244106fa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent 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 0b7845919d..9ff14d6bc1 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 @@ -3,7 +3,6 @@ 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.* import androidx.compose.material3.Icon @@ -14,21 +13,21 @@ 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.* +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.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.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme 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.MostlyUsedCard import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf @@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - 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, + earnFilterUM = state.earnFilterUM, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, ) @@ -113,8 +108,8 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { AnimatedContent( targetState = state, contentKey = { it::class.java }, - ) { st -> - when (st) { + ) { animatedState -> + when (animatedState) { is EarnListUM.Loading -> { MostlyUsedPlaceholder() } @@ -127,7 +122,7 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed( - items = st.items, + items = animatedState.items, key = { _, item -> "${item.tokenName}-${item.network}" }, ) { index, item -> val cardModifier = Modifier.conditional( @@ -151,84 +146,33 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 12.dp, - ), + .padding(horizontal = 16.dp, vertical = 12.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), contentAlignment = Alignment.Center, ) { - UnableToLoadData(onRetryClick = st.onRetryClicked) + UnableToLoadData(onRetryClick = animatedState.onRetryClicked) } } + EarnListUM.Empty -> Unit // no need to handle } } } -@Composable -private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .width(148.dp) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(onClick = onClick) - .padding(12.dp), - ) { - CurrencyIcon( - modifier = Modifier.size(32.dp), - state = item.currencyIconState, - shouldDisplayNetwork = true, - networkBadgeSize = 12.dp, - networkBadgeBackground = TangemTheme.colors.background.action, - ) - - SpacerH(8.dp) - - Row( - 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) - Text( - text = item.symbol.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - ) - } - - SpacerH(2.dp) - - Text( - text = item.earnValue.resolveReference(), - color = TangemTheme.colors.text.accent, - style = TangemTheme.typography.caption1, - maxLines = 1, - ) - } -} - @Composable private fun BestOpportunitiesFilters( state: EarnBestOpportunitiesUM, - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { when (state) { is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() else -> FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + earnFilterUM = earnFilterUM, onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, ) @@ -307,9 +251,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) @Composable private fun FilterButtons( - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, - isEnabled: Boolean, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, modifier: Modifier = Modifier, @@ -319,10 +261,14 @@ private fun FilterButtons( ) { SecondarySmallButton( config = SmallButtonConfig( - text = selectedNetworkFilterText, + text = when (earnFilterUM.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(earnFilterUM.selectedNetworkFilter.text) + }, onClick = onNetworkFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isNetworkFilterEnabled, ), ) @@ -330,10 +276,10 @@ private fun FilterButtons( SecondarySmallButton( config = SmallButtonConfig( - text = selectedTypeFilterText, + text = earnFilterUM.selectedTypeFilter.text, onClick = onTypeFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isTypeFilterEnabled, ), ) } @@ -496,7 +442,7 @@ private fun EarnContentLoadingPreview() { ) { EarnContent( state = previewEarnUM( - mostlyUsed = EarnListUM.Loading, + mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, ), ) @@ -588,8 +534,12 @@ private fun previewEarnUM( ): EarnUM = EarnUM( mostlyUsed = mostlyUsed, bestOpportunities = bestOpportunities, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + isTypeFilterEnabled = true, + isNetworkFilterEnabled = true, + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt new file mode 100644 index 0000000000..5ce51b2458 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -0,0 +1,196 @@ +package com.tangem.features.feed.ui.earn.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.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.SpacerW +import com.tangem.core.ui.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.features.feed.ui.earn.state.EarnListItemUM + +@Composable +internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + MostlyUsedCardV2( + modifier = modifier, + item = item, + onClick = onClick, + ) + } else { + MostlyUsedCardV1( + modifier = modifier, + item = item, + onClick = onClick, + ) + } +} + +@Composable +private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + OpportunitiesBG( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), + icon = TangemIconUM.Currency(item.currencyIconState), + ) { + Column(modifier = Modifier.padding(12.dp)) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(22.dp) + + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography2.bodySemibold16, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors2.text.status.positive, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + } +} + +@Composable +private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(onClick = onClick) + .padding(12.dp), + ) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(8.dp) + + Row( + 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) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV1() { + TangemThemePreview { + MostlyUsedCardV1( + 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 = {}, + ), + onClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV2() { + TangemThemePreviewRedesign { + MostlyUsedCardV2( + 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 = {}, + ), + onClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt new file mode 100644 index 0000000000..3cf3b0e582 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class EarnFilterUM( + val selectedTypeFilter: EarnFilterTypeUM, + val selectedNetworkFilter: EarnFilterNetworkUM, + val isTypeFilterEnabled: Boolean = true, + val isNetworkFilterEnabled: Boolean = true, +) \ 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 c4506586bc..72ba54139d 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 @@ -10,6 +10,7 @@ internal sealed interface EarnListUM { data object Loading : EarnListUM data class Content(val items: ImmutableList) : EarnListUM data class Error(val onRetryClicked: () -> Unit) : EarnListUM + data object Empty : EarnListUM } @Immutable 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 a11f66f48e..974b5a71b2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -1,20 +1,14 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference @Immutable internal data class EarnUM( val mostlyUsed: EarnListUM, val bestOpportunities: EarnBestOpportunitiesUM, - val selectedTypeFilter: EarnFilterTypeUM, - val selectedNetworkFilter: EarnFilterNetworkUM, + val earnFilterUM: EarnFilterUM, val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, val onSliderScroll: () -> Unit, -) { - - val selectedTypeFilterText: TextReference - get() = selectedTypeFilter.text -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt index c43c6b6a12..9fd9997c5b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.feed.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -8,12 +9,18 @@ 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.SpacerW 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) { +internal fun Header( + onSeeAllClick: () -> Unit, + isLoading: Boolean, + shouldShowSeeAll: Boolean, + title: @Composable () -> Unit, +) { AnimatedContent(isLoading) { animatedState -> Row( modifier = Modifier @@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title 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, - ), - ) + Box(modifier = Modifier.weight(1f)) { + title() + } + SpacerW(8.dp) + AnimatedVisibility(shouldShowSeeAll) { + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.common_see_all), + onClick = onSeeAllClick, + ), + ) + } } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index fb70686941..d97e6bd7a3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -7,6 +7,7 @@ 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.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R @@ -23,8 +24,8 @@ 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 +internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) { + if (earnListUM is EarnListUM.Empty) return Column(modifier = modifier) { Header( @@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif text = stringResourceSafe(R.string.markets_earn_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = onSeeAllClick, isLoading = earnListUM is EarnListUM.Loading, + shouldShowSeeAll = earnListUM is EarnListUM.Content, ) SpacerH(12.dp) @@ -53,6 +57,7 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items) is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked) EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) + EarnListUM.Empty -> Unit } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt index 1c2798cbf8..73294fe9f8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt @@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.MarketsListItemPlaceholder -import com.tangem.common.ui.news.DefaultLoadingArticle -import com.tangem.common.ui.news.TrendingLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index 1aa00e6976..11360bacd7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -4,12 +4,7 @@ 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.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.common.ui.markets.MarketsListItem @@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis text = stringResourceSafe(R.string.markets_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + shouldShowSeeAll = currentChart is MarketChartUM.Content, + isLoading = currentChart is MarketChartUM.Loading, ) SpacerH(12.dp) @@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall text = stringResourceSafe(R.string.markets_pulse_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { onSeeAllClick() }, + shouldShowSeeAll = true, + isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading, ) LazyRow( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index e2fa4ecbf6..4601283941 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -2,28 +2,26 @@ 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.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.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.style.TextOverflow 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.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW @@ -31,16 +29,18 @@ 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.LocalRedesignEnabled 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 +import com.tangem.features.feed.ui.feed.state.* -private const val FOURTH_ITEM_INDEX = 3 +internal 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) +private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF +private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF + +private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC +private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED @Composable internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { @@ -64,13 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend @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 } + val isRedesignEnabled = LocalRedesignEnabled.current + val gradientStart = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_FIRST_PART_V2) + } else { + Color(LINEAR_GRADIENT_FIRST_PART_V1) + } } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) + + val gradientEnd = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_SECOND_PART_V2) + } else { + Color(LINEAR_GRADIENT_SECOND_PART_V1) + } } + Column { Header( title = { @@ -95,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, withStyle( SpanStyle().copy( brush = Brush.linearGradient( - GRADIENT_START to LinearGradientFirstPart, - GRADIENT_END to LinearGradientSecondPart, + GRADIENT_START to gradientStart, + GRADIENT_END to gradientEnd, ), ), ) { @@ -104,10 +114,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } }, style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, + isLoading = news.newsUMState == NewsUMState.LOADING, + shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT, ) SpacerH(12.dp) @@ -125,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } } - 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), - ) - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) }, + onSliderScroll = feedListCallbacks.onSliderScroll, + onSliderEndReached = feedListCallbacks.onSliderEndReached, + onArticleClick = feedListCallbacks.onArticleClick, + ), + content = news.content, + shouldShowSeeAllNewsItem = true, + ), + ) - 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) } } @@ -181,10 +166,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) { text = stringResourceSafe(R.string.common_news), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = {}, + shouldShowSeeAll = false, + isLoading = false, ) SpacerH(12.dp) BlockCard( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt new file mode 100644 index 0000000000..87a5053d61 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt @@ -0,0 +1,70 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.background +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +@Suppress("LongMethod") +@Composable +internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) { + val background = LocalMainBottomSheetColor.current.value + LazyRow( + modifier = Modifier.background(color = background), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + } else { + Modifier + } + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .width(228.dp) + .heightIn(min = 172.dp) + .fillMaxHeight(), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .width(228.dp) + .heightIn(min = 172.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt new file mode 100644 index 0000000000..48d21b699b --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt @@ -0,0 +1,30 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.material3.CardColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { + if (LocalRedesignEnabled.current) { + ArticleCardV2( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + ) + } else { + ArticleCardV1( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + colors = colors, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt index a6a7a2a078..97ede1fbdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import android.content.res.Configuration import androidx.compose.foundation.* @@ -37,7 +37,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @Composable -fun ArticleCard( +internal fun ArticleCardV1( articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier, @@ -126,7 +126,7 @@ private fun TrendingArticle( } @Composable -fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { +internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) { BlockCard( modifier = modifier, onClick = onClick, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt new file mode 100644 index 0000000000..d55d4773c2 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt @@ -0,0 +1,405 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import android.content.res.Configuration +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.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.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +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.SpacerW +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet + +@Composable +internal fun ArticleCardV2( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (articleConfigUM.isTrending) { + TrendingArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } else { + DefaultArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } +} + +@Composable +private fun TrendingArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TrendingArticleBackground( + modifier = modifier, + onClick = onArticleClick, + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + ) { + DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}")) + + SpacerH(8.dp) + + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.headingSemibold20, + textAlign = TextAlign.Start, + ) + + SpacerH(18.dp) + + Text( + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + SpacerH(18.dp) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } + } +} + +@Composable +internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(20.dp)) + .background(color = TangemTheme.colors2.surface.level3) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(20.dp), + ) + .padding(vertical = 41.dp, horizontal = 16.dp), + ) { + Image( + modifier = Modifier.size(40.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48), + contentDescription = stringResourceSafe(R.string.common_show_more), + ) + + SpacerH(10.dp) + + Text( + text = stringResourceSafe(R.string.news_all_news), + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + ) + + SpacerH(4.dp) + + Text( + text = stringResourceSafe(R.string.news_stay_in_the_loop), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } +} + +@Composable +private fun DefaultArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .background(color = TangemTheme.colors2.surface.level3) + .clickable(onClick = onArticleClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(20.dp), + ) + .padding(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RatingInfo( + rating = stringReference("${articleConfigUM.score}"), + isTrending = false, + ) + } + + SpacerH(8.dp) + + Text( + modifier = Modifier.weight(1f), + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.bodyRegular16, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + + SpacerH(8.dp) + + Text( + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + SpacerH(8.dp) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } +} + +@Suppress("MagicNumber") +@Composable +private fun TrendingArticleBackground( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val isDarkTheme = LocalIsInDarkTheme.current + + val bgColor = remember(isDarkTheme) { + if (isDarkTheme) { + Color(TRENDING_NIGHT_BG) + } else { + Color(TRENDING_LIGHT_BG) + } + } + + Box( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .drawBehind { + drawRect(bgColor) + + val w = size.width + val h = size.height + val radiusScale = (w + h) / 2f + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF7C16F1).copy(alpha = .8f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.4f * h), + radius = radiusScale * 1.57f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF3360FF).copy(alpha = .7f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.95f * h), + radius = radiusScale * 1.9f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFF9408).copy(alpha = .45f), + Color.Transparent, + ), + center = Offset(-0.38f * w, 2.1f * h), + radius = radiusScale * 1.41f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFC2424).copy(alpha = .5f), + Color.Transparent, + ), + center = Offset(1.188f * w, 2.37f * h), + radius = radiusScale * 1.41f, + ), + ) + } + .border(width = 1.dp, color = bgColor.copy(.1f)) + .clickable(onClick = onClick), + content = content, + ) +} + +@Composable +private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + RatingInfo(rating = rating, isTrending = true) + + SpacerW(8.dp) + + Text( + text = stringResourceSafe(R.string.feed_trending_now), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + ) + } +} + +@Composable +private fun RatingInfo(rating: TextReference, isTrending: Boolean) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16), + tint = if (isTrending) { + TangemTheme.colors2.fill.status.attention + } else { + TangemTheme.colors2.markers.iconGray + }, + contentDescription = null, + ) + + SpacerW(2.dp) + + Text( + text = rating.resolveReference(), + color = if (isTrending) { + TangemTheme.colors2.text.status.attention + } else { + TangemTheme.colors2.text.neutral.secondary + }, + style = TangemTheme.typography2.captionSemibold12, + ) +} + +private const val TRENDING_NIGHT_BG = 0xFF1F1F1F +private const val TRENDING_LIGHT_BG = 0xFFFFFFFF + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TagsPreview() { + TangemThemePreviewRedesign { + Tags( + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), + ) + } +} + +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO) +@Composable +private fun ArticleCardsPreview() { + val tags = listOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Breaking news")), + ).toImmutableSet() + + val config = ArticleConfigUM( + id = 1, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + score = 9.5f, + createdAt = TextReference.Str("1h ago"), + isTrending = true, + tags = tags, + isViewed = false, + ) + + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + ) { + ArticleCardV2( + articleConfigUM = config, + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false, isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ShowMoreArticlesCardV2(onClick = {}) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt similarity index 76% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt index 46714a0ba8..fde217b1de 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt @@ -1,9 +1,11 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableSet +@Immutable data class ArticleConfigUM( val id: Int, val title: String, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt new file mode 100644 index 0000000000..fadece2c4e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt @@ -0,0 +1,258 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.ds.badge.TangemBadge +import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.core.ui.ds.badge.TangemBadgeIconPosition +import com.tangem.core.ui.ds.badge.TangemBadgeShape +import com.tangem.core.ui.ds.badge.TangemBadgeSize +import com.tangem.core.ui.ds.badge.TangemBadgeType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ArticleHeader( + isTrending: Boolean, + title: String, + createdAt: String, + score: Float, + tags: ImmutableList, + modifier: Modifier = Modifier, +) { + if (LocalRedesignEnabled.current) { + ArticleHeaderV2( + isTrending = isTrending, + title = title, + createdAt = createdAt, + score = score, + tags = tags, + modifier = modifier, + ) + } else { + ArticleHeaderV1( + title = title, + createdAt = createdAt, + score = score, + tags = tags, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ArticleHeaderV1( + title: String, + createdAt: String, + score: Float, + tags: ImmutableList, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + ArticleInfo( + score = score, + createdAt = createdAt, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = title, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + if (tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + tags.forEach { tag -> + Label( + state = tag, + ) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ArticleHeaderV2( + isTrending: Boolean, + title: String, + createdAt: String, + score: Float, + tags: ImmutableList, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row( + modifier = Modifier + .heightIn(min = 66.dp) + .padding(top = 16.dp), + verticalAlignment = Alignment.Bottom, + ) { + DateBlock( + modifier = Modifier.weight(1f), + createdAt = createdAt, + ) + SpacerW(30.dp) + VerticalDivider( + modifier = Modifier + .height(46.dp) + .padding(bottom = 4.dp), + color = TangemTheme.colors2.border.neutral.primary, + ) + SpacerW(30.dp) + ScoreBlock( + modifier = Modifier.weight(1f), + score = score, + isTrending = isTrending, + ) + } + + Text( + modifier = Modifier.padding(vertical = 36.dp), + text = title, + style = TangemTheme.typography2.headingBold34, + color = TangemTheme.colors2.text.neutral.primary, + ) + + if (tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + tags.forEach { tag -> + TangemBadge( + text = tag.text, + tangemIconUM = when (val content = tag.leadingContent) { + LabelLeadingContentUM.None -> null + is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl) + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X9, + type = TangemBadgeType.Tinted, + color = TangemBadgeColor.Gray, + iconPosition = when (tag.leadingContent) { + LabelLeadingContentUM.None -> TangemBadgeIconPosition.None + is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start + }, + ) + } + } + } + } +} + +@Composable +private fun ScoreBlock(score: Float, isTrending: Boolean, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16), + tint = if (isTrending) { + TangemTheme.colors2.fill.status.attention + } else { + TangemTheme.colors2.graphic.neutral.primary + }, + contentDescription = null, + ) + Text( + text = score.toString(), + style = TangemTheme.typography2.bodyRegular16, + color = if (isTrending) { + TangemTheme.colors2.text.status.attention + } else { + TangemTheme.colors2.text.neutral.primary + }, + ) + } + Text( + text = stringResourceSafe(R.string.news_trending_score), + style = TangemTheme.typography2.captionSemibold13, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + } +} + +@Composable +private fun DateBlock(createdAt: String, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_calendar_20), + tint = TangemTheme.colors2.fill.neutral.primary, + contentDescription = null, + ) + Text( + text = createdAt, + style = TangemTheme.typography2.captionSemibold13, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ArticleHeaderPreviewV1() { + TangemThemePreview { + ArticleHeader( + title = "Something going good!", + createdAt = "1 hour ago", + score = 5.5f, + tags = persistentListOf(), + isTrending = true, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ArticleHeaderPreviewV2() { + TangemThemePreviewRedesign { + ArticleHeader( + title = "Something going good!", + createdAt = "1 hour ago", + score = 5.5f, + tags = persistentListOf(), + isTrending = true, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt index a2834e2afb..656b128605 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt new file mode 100644 index 0000000000..74fbda7a34 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt @@ -0,0 +1,220 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +fun TrendingLoadingArticle(modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + TrendingLoadingArticleV2(modifier) + } else { + TrendingLoadingArticleV1(modifier) + } +} + +@Composable +private fun TrendingLoadingArticleV1(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp) + SpacerH(12.dp) + RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp) + SpacerH(14.dp) + RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp) + SpacerH(32.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + } + } + } +} + +@Composable +private fun TrendingLoadingArticleV2(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(48.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(12.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + } +} + +@Composable +fun DefaultLoadingArticle(modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + DefaultLoadingArticleV2(modifier) + } else { + DefaultLoadingArticleV1(modifier) + } +} + +@Composable +private fun DefaultLoadingArticleV1(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(28.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 72.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 62.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 32.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + } +} + +@Composable +private fun DefaultLoadingArticleV2(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(44.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(12.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 72.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 62.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 32.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TrendingLoadingArticlePreviewV1() { + TangemThemePreview { + TrendingLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TrendingLoadingArticlePreviewV2() { + TangemThemePreviewRedesign { + TrendingLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DefaultLoadingArticlePreviewV1() { + TangemThemePreview { + DefaultLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DefaultLoadingArticlePreviewV2() { + TangemThemePreviewRedesign { + DefaultLoadingArticle() + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt new file mode 100644 index 0000000000..fba589144d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { + val isRedesignEnabled: Boolean = LocalRedesignEnabled.current + if (isRedesignEnabled) { + ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick) + } else { + ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt similarity index 71% rename from common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt index a163c8ee15..f5ec98879e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -8,8 +8,12 @@ import androidx.compose.ui.layout.SubcomposeMeasureScope import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList @@ -31,7 +35,25 @@ internal fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { val tagPlaceables = subcompose(ContentSlot.Tags) { tags.forEach { tag -> - Label(state = tag) + if (LocalRedesignEnabled.current) { + TangemBadge( + text = tag.text, + tangemIconUM = when (val content = tag.leadingContent) { + LabelLeadingContentUM.None -> null + is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl) + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X6, + type = TangemBadgeType.Tinted, + color = TangemBadgeColor.Gray, + iconPosition = when (tag.leadingContent) { + LabelLeadingContentUM.None -> TangemBadgeIconPosition.None + is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start + }, + ) + } else { + Label(state = tag) + } } }.map { it.measure(constraints) } @@ -108,12 +130,22 @@ private fun SubcomposeMeasureScope.calculateLayoutInfo( @Composable private fun OverflowLabel(count: Int) { - Label( - state = LabelUM( + if (LocalRedesignEnabled.current) { + TangemBadge( text = TextReference.Str("${StringsSigns.PLUS}$count"), - maxLines = 1, - ), - ) + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X6, + type = TangemBadgeType.Tinted, + color = TangemBadgeColor.Gray, + ) + } else { + Label( + state = LabelUM( + text = TextReference.Str("${StringsSigns.PLUS}$count"), + maxLines = 1, + ), + ) + } } private fun calculateRowWidth(placeables: List, spacingPx: Int): Int { 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 ceaf16a400..59635e4fe6 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 @@ -2,7 +2,7 @@ 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.features.feed.ui.feed.components.articles.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 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 8d8a014175..8ba119a2d1 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 @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.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 @@ -18,7 +18,7 @@ internal data class FeedListUM( val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, val globalState: GlobalFeedState = GlobalFeedState.Content, - val earnListUM: EarnListUM?, + val earnListUM: EarnListUM, ) internal data class FeedListCallbacks( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt new file mode 100644 index 0000000000..f61adb2d56 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.ui.feed.state + +import androidx.compose.runtime.Immutable +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class NewsSliderConfig( + val shouldShowSeeAllNewsItem: Boolean, + val content: ImmutableList, + val callbacks: NewsSliderCallbacks, +) + +@Immutable +internal data class NewsSliderCallbacks( + val onOpenAllNews: () -> Unit, + val onSliderScroll: () -> Unit, + val onSliderEndReached: () -> Unit, + val onArticleClick: (id: Int) -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 77c6da0b24..67cc815484 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,7 +42,6 @@ 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 @@ -50,13 +49,13 @@ import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.distinctUntilChanged +import com.tangem.core.ui.R as CoreR @Suppress("LongParameterList") @Composable internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, backgroundColor: Color, - isAccountEnabled: Boolean, modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { @@ -65,7 +64,6 @@ internal fun MarketsTokenDetailsContent( backgroundColor = backgroundColor, state = state, portfolioBlock = portfolioBlock, - isAccountEnabled = isAccountEnabled, ) when (state.bottomSheetConfig.content) { @@ -80,7 +78,6 @@ internal fun MarketsTokenDetailsContent( private fun Content( state: MarketsTokenDetailsUM, backgroundColor: Color, - isAccountEnabled: Boolean, modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { @@ -134,7 +131,6 @@ private fun Content( tokenMarketDetailsBody( state = state.body, - isAccountEnabled = isAccountEnabled, portfolioBlock = portfolioBlock, relatedNews = state.relatedNews, ) @@ -322,7 +318,6 @@ private fun MarketsTokenDetailsContent_Preview( state = params, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, - isAccountEnabled = true, ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index ff3047cac8..a0457c58ec 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -2,34 +2,27 @@ package com.tangem.features.feed.ui.market.detailed.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope -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.layout.onFirstVisible import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.items.DescriptionPlaceholder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.components.NewsSlider +import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews -private const val FOURTH_ITEM_INDEX = 3 - -@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA] +@Suppress("CanBeNonNullable") internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, - isAccountEnabled: Boolean, portfolioBlock: @Composable ((Modifier) -> Unit)?, relatedNews: RelatedNews, ) { @@ -45,9 +38,7 @@ internal fun LazyListScope.tokenMarketDetailsBody( } } - if (isAccountEnabled) { - aboutCoinHeader() - } + aboutCoinHeader() loadingInfoBlocks() } @@ -66,9 +57,7 @@ internal fun LazyListScope.tokenMarketDetailsBody( relatedNews(relatedNews) } - if (isAccountEnabled) { - aboutCoinHeader() - } + aboutCoinHeader() infoBlocksList(state.infoBlocks) } @@ -207,13 +196,6 @@ private fun LazyListScope.loadingInfoBlocks() { private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { item("related-news") { - val listState = rememberLazyListState() - val articlesReadStatus = remember(relatedNews.articles) { - relatedNews.articles.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } Column( modifier = Modifier .fillMaxWidth() @@ -231,35 +213,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { color = TangemTheme.colors.text.primary1, ) - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = relatedNews.articles, - key = { index, article -> article.id }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = relatedNews.onScroll, - ) - } else { - Modifier - } - - ArticleCard( - articleConfigUM = article, - onArticleClick = { relatedNews.onArticledClicked(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = {}, // not applicable here + onSliderScroll = relatedNews.onScroll, + onSliderEndReached = {}, // not applicable here + onArticleClick = relatedNews.onArticledClicked, + ), + content = relatedNews.articles, + shouldShowSeeAllNewsItem = false, + ), + ) } } } 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 d0c4775d82..5f21370028 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.market.detailed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 41d152a94b..518b25e6a6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -3,53 +3,25 @@ package com.tangem.features.feed.ui.news.details 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.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.SubcomposeAsyncImage -import coil.request.CachePolicy -import coil.request.ImageRequest -import com.tangem.common.ui.news.ArticleHeader -import com.tangem.core.ui.R -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.pager.PagerIndicator -import com.tangem.core.ui.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.extensions.conditionalCompose +import com.tangem.core.ui.res.* +import com.tangem.features.feed.ui.news.details.components.ArticleDetail import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder -import com.tangem.features.feed.ui.news.details.components.RelatedTokensBlock -import com.tangem.features.feed.ui.news.details.state.* +import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM +import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory +import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM @Composable internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) { @@ -86,6 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif @Composable private fun Content(state: NewsDetailsUM, background: Color) { + val isRedesignEnabled = LocalRedesignEnabled.current val pagerState = rememberPagerState( initialPage = state.selectedArticleIndex, pageCount = { state.articles.size }, @@ -109,6 +82,12 @@ private fun Content(state: NewsDetailsUM, background: Color) { Column( modifier = Modifier .fillMaxSize() + .conditionalCompose( + condition = isRedesignEnabled, + modifier = { + hazeSourceTangem(zIndex = 1f) + }, + ) .background(background), ) { Box(modifier = Modifier.fillMaxSize()) { @@ -138,242 +117,6 @@ private fun Content(state: NewsDetailsUM, background: Color) { } } -@Suppress("LongMethod") -@Composable -private fun ArticleDetail( - article: ArticleUM, - onLikeClick: () -> Unit, - relatedTokensUM: RelatedTokensUM, - modifier: Modifier = Modifier, -) { - val hapticFeedback = LocalHapticFeedback.current - val density = LocalDensity.current - val background = LocalMainBottomSheetColor.current.value - val pagerHeight = 32.dp - val contentPadding = pagerHeight + 56.dp + with(density) { - WindowInsets.navigationBars.getBottom(this).div(this.density) - }.dp - - Box(modifier = modifier) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = contentPadding), - ) { - item("content") { - ArticleHeader( - title = article.title, - createdAt = article.createdAt.resolveReference(), - score = article.score, - tags = article.tags, - modifier = Modifier - .padding(top = 16.dp) - .padding(horizontal = 16.dp), - ) - - if (article.shortContent.isNotEmpty()) { - QuickRecap( - content = article.shortContent, - modifier = Modifier - .padding(top = 32.dp) - .padding(horizontal = 16.dp), - ) - } - - Text( - text = article.content, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(top = 16.dp) - .padding(horizontal = 16.dp), - ) - - SpacerH(24.dp) - - SecondaryButtonIconStart( - modifier = Modifier.padding(horizontal = 16.dp), - iconResId = if (article.isLiked) { - R.drawable.ic_heart_filled_20 - } else { - R.drawable.ic_heart_20 - }, - iconTint = Color.Unspecified, - text = stringResourceSafe(R.string.news_like), - size = TangemButtonSize.RoundedAction, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onLikeClick() - }, - ) - - RelatedTokensBlock( - relatedTokensUM = relatedTokensUM, - onItemClick = when (relatedTokensUM) { - is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick - else -> null - }, - modifier = Modifier.padding(horizontal = 16.dp), - ) - - if (article.relatedArticles.isNotEmpty()) { - SpacerH(24.dp) - Row( - modifier = Modifier.padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResourceSafe(R.string.news_sources), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = "${article.relatedArticles.size}", - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.tertiary, - ) - } - } - } - - if (article.relatedArticles.isNotEmpty()) { - item("relatedArticles") { - LazyRow( - modifier = Modifier.padding(vertical = 12.dp), - state = rememberLazyListState(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - items = article.relatedArticles, - key = RelatedArticleUM::id, - ) { article -> - RelatedNewsItem( - relatedArticle = article, - modifier = Modifier.fillParentMaxHeight(), - ) - } - } - } - } - } - BottomFade( - modifier = Modifier - .align(Alignment.BottomCenter), - backgroundColor = background, - ) - } -} - -@Composable -private fun QuickRecap(content: String, modifier: Modifier = Modifier) { - Box( - modifier = modifier.height(IntrinsicSize.Min), - ) { - VerticalDivider( - modifier = Modifier.fillMaxHeight(), - thickness = 2.dp, - color = TangemTheme.colors.stroke.primary, - ) - Column( - modifier = Modifier - .padding(start = 16.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_quick_recap_16), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - modifier = Modifier.size(20.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResourceSafe(R.string.news_quick_recap), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.accent, - ) - } - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = content, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - } - } -} - -@Composable -private fun RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .sizeIn(maxWidth = 256.dp, minHeight = 132.dp) - .background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp)) - .clickable(onClick = relatedArticle.onClick) - .padding(12.dp), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Column(modifier = Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp)) { - Icon( - painter = painterResource(id = R.drawable.ic_explore_16), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier.size(16.dp), - ) - SpacerW(4.dp) - Text( - text = relatedArticle.media.name, - style = TangemTheme.typography.caption1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = TangemTheme.colors.text.tertiary, - ) - } - if (relatedArticle.title.isNotEmpty()) { - SpacerH(4.dp) - Text( - text = relatedArticle.title, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } - if (relatedArticle.imageUrl != null) { - SubcomposeAsyncImage( - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(4.dp)), - contentScale = ContentScale.Crop, - model = ImageRequest.Builder(context = LocalContext.current) - .data(relatedArticle.imageUrl) - .crossfade(enable = false) - .allowHardware(true) - .memoryCachePolicy(CachePolicy.DISABLED) - .build(), - loading = { - RectangleShimmer( - modifier = Modifier.size(40.dp), - radius = 4.dp, - ) - }, - error = {}, - contentDescription = relatedArticle.media.name, - ) - } - } - SpacerHMax() - Text( - text = relatedArticle.publishedAt.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -396,4 +139,28 @@ private fun PreviewNewsDetailsContent() { ) } } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewNewsDetailsContentV2() { + TangemThemePreviewRedesign { + val background = TangemTheme.colors.background.tertiary + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, + ) { + NewsDetailsContent( + state = NewsDetailsUM( + articlesStateUM = ArticlesStateUM.Content, + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onShareClick = {}, + onLikeClick = {}, + onBackClick = {}, + onArticleIndexChanged = {}, + ), + ) + } + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt new file mode 100644 index 0000000000..12a7153f8d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt @@ -0,0 +1,324 @@ +package com.tangem.features.feed.ui.news.details.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.BottomFadeWithBlur +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader +import com.tangem.features.feed.ui.news.details.state.ArticleUM +import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM +import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM + +@Composable +internal fun ArticleDetail( + article: ArticleUM, + onLikeClick: () -> Unit, + relatedTokensUM: RelatedTokensUM, + modifier: Modifier = Modifier, +) { + if (LocalRedesignEnabled.current) { + ArticleDetailV2( + article = article, + onLikeClick = onLikeClick, + relatedTokensUM = relatedTokensUM, + modifier = modifier, + ) + } else { + ArticleDetailV1( + article = article, + onLikeClick = onLikeClick, + relatedTokensUM = relatedTokensUM, + modifier = modifier, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun ArticleDetailV1( + article: ArticleUM, + onLikeClick: () -> Unit, + relatedTokensUM: RelatedTokensUM, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val density = LocalDensity.current + val background = LocalMainBottomSheetColor.current.value + val pagerHeight = 32.dp + val contentPadding = pagerHeight + 56.dp + with(density) { + WindowInsets.navigationBars.getBottom(this).div(this.density) + }.dp + + Box(modifier = modifier) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = contentPadding), + ) { + item("content") { + ArticleHeader( + title = article.title, + createdAt = article.createdAt.resolveReference(), + score = article.score, + tags = article.tags, + isTrending = article.isTrending, + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp), + ) + + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier + .padding(top = 32.dp) + .padding(horizontal = 16.dp), + ) + } + + Text( + text = article.content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp), + ) + + SpacerH(24.dp) + + SecondaryButtonIconStart( + modifier = Modifier.padding(horizontal = 16.dp), + iconResId = if (article.isLiked) { + R.drawable.ic_heart_filled_20 + } else { + R.drawable.ic_heart_20 + }, + iconTint = Color.Unspecified, + text = stringResourceSafe(R.string.news_like), + size = TangemButtonSize.RoundedAction, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onLikeClick() + }, + ) + + RelatedTokensBlock( + relatedTokensUM = relatedTokensUM, + onItemClick = when (relatedTokensUM) { + is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick + else -> null + }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + if (article.relatedArticles.isNotEmpty()) { + SpacerH(24.dp) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResourceSafe(R.string.news_sources), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = "${article.relatedArticles.size}", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + if (article.relatedArticles.isNotEmpty()) { + item("relatedArticles") { + LazyRow( + modifier = Modifier.padding(vertical = 12.dp), + state = rememberLazyListState(), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = article.relatedArticles, + key = RelatedArticleUM::id, + ) { article -> + RelatedNewsItem( + relatedArticle = article, + modifier = Modifier.fillParentMaxHeight(), + ) + } + } + } + } + } + BottomFade( + modifier = Modifier + .align(Alignment.BottomCenter), + backgroundColor = background, + ) + } +} + +@Suppress("LongMethod") +@Composable +internal fun ArticleDetailV2( + article: ArticleUM, + onLikeClick: () -> Unit, + relatedTokensUM: RelatedTokensUM, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val density = LocalDensity.current + val background = LocalMainBottomSheetColor.current.value + val pagerHeight = 32.dp + val contentPadding = pagerHeight + 56.dp + with(density) { + WindowInsets.navigationBars.getBottom(this).div(this.density) + }.dp + + Box(modifier = modifier) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = 0f) + .background(background), + contentPadding = PaddingValues(bottom = contentPadding), + ) { + item("content") { + ArticleHeader( + title = article.title, + createdAt = article.createdAt.resolveReference(), + score = article.score, + tags = article.tags, + isTrending = article.isTrending, + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp), + ) + + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier + .padding(top = 32.dp) + .padding(horizontal = 16.dp), + ) + } + + Text( + text = article.content, + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + modifier = Modifier + .padding(top = 12.dp) + .padding(horizontal = 16.dp), + ) + + SpacerH(24.dp) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 24.dp), + color = TangemTheme.colors2.border.neutral.primary, + ) + + SpacerH(20.dp) + + SecondaryTangemButton( + modifier = Modifier.padding(horizontal = 24.dp), + text = resourceReference(R.string.news_like), + size = com.tangem.core.ui.ds.button.TangemButtonSize.X9, + iconRes = if (article.isLiked) { + R.drawable.ic_like_20 + } else { + R.drawable.ic_heart_20 + }, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onLikeClick() + }, + shape = TangemButtonShape.Rounded, + ) + + RelatedTokensBlock( + relatedTokensUM = relatedTokensUM, + onItemClick = when (relatedTokensUM) { + is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick + else -> null + }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + if (article.relatedArticles.isNotEmpty()) { + SpacerH(24.dp) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResourceSafe(R.string.news_sources), + style = TangemTheme.typography2.headingSemibold20, + color = TangemTheme.colors2.text.neutral.primary, + ) + } + } + } + + if (article.relatedArticles.isNotEmpty()) { + item("relatedArticles") { + LazyRow( + modifier = Modifier + .padding(vertical = 12.dp), + state = rememberLazyListState(), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = article.relatedArticles, + key = RelatedArticleUM::id, + ) { article -> + RelatedNewsItem( + relatedArticle = article, + modifier = Modifier.fillParentMaxHeight(), + ) + } + } + } + } + } + + BottomFadeWithBlur( + modifier = Modifier + .align(Alignment.BottomCenter) + .height(80.dp) + .fillMaxWidth(), + backgroundColor = background, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt index e985004e1b..a814d22eed 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt @@ -1,23 +1,45 @@ package com.tangem.features.feed.ui.news.details.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + NewsDetailsPlaceholderV2(background, modifier) + } else { + NewsDetailsPlaceholderV1(background, modifier) + } +} + +@Suppress("LongMethod") +@Composable +private fun NewsDetailsPlaceholderV1(background: Color, modifier: Modifier = Modifier) { Column( - modifier = modifier.fillMaxSize().background(background).padding(16.dp), + modifier = modifier + .fillMaxSize() + .background(background) + .padding(16.dp), ) { RectangleShimmer(modifier = Modifier.size(width = 112.dp, height = 20.dp)) SpacerH(8.dp) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(28.dp), + modifier = Modifier + .fillMaxWidth() + .height(28.dp), ) SpacerH(4.dp) RectangleShimmer(modifier = Modifier.size(height = 28.dp, width = 208.dp)) @@ -29,29 +51,178 @@ fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(8.dp), ) { RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 24.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 24.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 70.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 70.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 96.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 96.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 100.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 100.dp), ) } } +} + +@Suppress("LongMethod") +@Composable +private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background(background) + .padding(16.dp), + ) { + Row( + modifier = Modifier.height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp), + ) { + Column { + RectangleShimmer( + modifier = Modifier.size(width = 50.dp, height = 20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(10.dp) + RectangleShimmer( + modifier = Modifier.size(width = 90.dp, height = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + } + + VerticalDivider(color = TangemTheme.colors2.border.neutral.primary) + + Column { + RectangleShimmer( + modifier = Modifier.size(width = 50.dp, height = 20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(10.dp) + RectangleShimmer( + modifier = Modifier.size(width = 90.dp, height = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + + SpacerH(36.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + radius = TangemTheme.dimens2.x25, + ) + + SpacerH(12.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(end = 106.dp), + radius = TangemTheme.dimens2.x25, + ) + + SpacerH(36.dp) + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 98.dp, height = 36.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 98.dp, height = 36.dp), + radius = TangemTheme.dimens2.x25, + ) + } + + SpacerH(20.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 22.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 66.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 46.dp), + radius = TangemTheme.dimens2.x25, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewsDetailsPlaceholderPreviewV1() { + TangemThemePreview { + NewsDetailsPlaceholder(background = TangemTheme.colors.background.tertiary) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewsDetailsPlaceholderPreviewV2() { + TangemThemePreviewRedesign { + NewsDetailsPlaceholder(background = TangemTheme.colors2.surface.level3) + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt new file mode 100644 index 0000000000..6cc0ec1434 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt @@ -0,0 +1,144 @@ +package com.tangem.features.feed.ui.news.details.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import 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.SpacerW +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun QuickRecap(content: String, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + QuickRecapV2(content, modifier) + } else { + QuickRecapV1(content, modifier) + } +} + +@Composable +private fun QuickRecapV1(content: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(IntrinsicSize.Min), + ) { + VerticalDivider( + modifier = Modifier + .fillMaxHeight() + .padding(start = 8.dp), + thickness = 2.dp, + color = TangemTheme.colors.stroke.primary, + ) + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_quick_recap_16), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResourceSafe(R.string.news_quick_recap), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.accent, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun QuickRecapV2(content: String, modifier: Modifier = Modifier) { + Column(modifier = modifier.height(IntrinsicSize.Min)) { + Row(verticalAlignment = Alignment.CenterVertically) { + 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 Color(LINEAR_GRADIENT_FIRST_PART), + GRADIENT_END to Color(LINEAR_GRADIENT_SECOND_PART), + ), + ), + ) { + append(stringResourceSafe(R.string.news_quick_recap)) + } + }, + style = TangemTheme.typography2.bodyRegular14, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + + SpacerH(10.dp) + + Box { + VerticalDivider( + modifier = Modifier + .fillMaxHeight() + .padding(start = 10.dp), + thickness = 2.dp, + color = Color(QUICK_RECAP_DIVIDER_COLOR), + ) + Text( + modifier = Modifier.padding(start = 20.dp), + text = content, + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun QuickRecapPreview() { + TangemThemePreviewRedesign { + QuickRecapV2( + content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut" + + " labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " + + "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit " + + "esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt " + + "in culpa qui officia deserunt mollit anim id est laborum.", + ) + } +} + +private const val QUICK_RECAP_DIVIDER_COLOR = 0xFFA99FFF +private const val LINEAR_GRADIENT_FIRST_PART = 0xFFA3A0FF +private const val LINEAR_GRADIENT_SECOND_PART = 0xFFF79DFF +private const val GRADIENT_START = 0f +private const val GRADIENT_END = 0.5f \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedNewsItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedNewsItem.kt new file mode 100644 index 0000000000..a3810b1bda --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedNewsItem.kt @@ -0,0 +1,188 @@ +package com.tangem.features.feed.ui.news.details.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM + +@Composable +internal fun RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + RelatedNewsItemV2(relatedArticle, modifier) + } else { + RelatedNewsItemV1(relatedArticle, modifier) + } +} + +@Composable +private fun RelatedNewsItemV1(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .sizeIn(maxWidth = 256.dp, minHeight = 132.dp) + .background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp)) + .clickable(onClick = relatedArticle.onClick) + .padding(12.dp), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp)) { + Icon( + painter = painterResource(id = R.drawable.ic_explore_16), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(16.dp), + ) + SpacerW(4.dp) + Text( + text = relatedArticle.media.name, + style = TangemTheme.typography.caption1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = TangemTheme.colors.text.tertiary, + ) + } + if (relatedArticle.title.isNotEmpty()) { + SpacerH(4.dp) + Text( + text = relatedArticle.title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (relatedArticle.imageUrl != null) { + SubcomposeAsyncImage( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop, + model = ImageRequest.Builder(context = LocalContext.current) + .data(relatedArticle.imageUrl) + .crossfade(enable = false) + .allowHardware(true) + .memoryCachePolicy(CachePolicy.DISABLED) + .build(), + loading = { + RectangleShimmer( + modifier = Modifier.size(40.dp), + radius = 4.dp, + ) + }, + error = {}, + contentDescription = relatedArticle.media.name, + ) + } + } + SpacerHMax() + Text( + text = relatedArticle.publishedAt.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun RelatedNewsItemV2(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .sizeIn(maxWidth = 228.dp, minHeight = 160.dp) + .background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ) + .clickable(onClick = relatedArticle.onClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ) + .padding(16.dp), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp)) { + Icon( + painter = painterResource(id = R.drawable.ic_explore_16), + contentDescription = null, + tint = TangemTheme.colors2.markers.iconGray, + modifier = Modifier.size(16.dp), + ) + SpacerW(2.dp) + Text( + text = relatedArticle.media.name, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } + if (relatedArticle.title.isNotEmpty()) { + SpacerH(8.dp) + Text( + text = relatedArticle.title, + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (relatedArticle.imageUrl != null) { + SubcomposeAsyncImage( + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop, + model = ImageRequest.Builder(context = LocalContext.current) + .data(relatedArticle.imageUrl) + .crossfade(enable = false) + .allowHardware(true) + .memoryCachePolicy(CachePolicy.DISABLED) + .build(), + loading = { + RectangleShimmer( + modifier = Modifier.size(44.dp), + radius = 12.dp, + ) + }, + error = {}, + contentDescription = relatedArticle.media.name, + ) + } + } + SpacerHMax() + Text( + text = relatedArticle.publishedAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedTokensBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedTokensBlock.kt index d43ebbb5a9..ce6a92843e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedTokensBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/RelatedTokensBlock.kt @@ -2,6 +2,7 @@ package com.tangem.features.feed.ui.news.details.components 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.runtime.remember @@ -16,6 +17,7 @@ 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.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.model.news.details.NewsDetailsModel.Companion.RELATED_TOKEN_MAX_COUNT import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM @@ -38,11 +40,20 @@ internal fun RelatedTokensBlock( Column(modifier = modifier) { SpacerH(40.dp) - Text( - text = stringResourceSafe(R.string.news_related_tokens), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) + if (LocalRedesignEnabled.current) { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = stringResourceSafe(R.string.news_related_tokens), + style = TangemTheme.typography2.headingSemibold20, + color = TangemTheme.colors2.text.neutral.primary, + ) + } else { + Text( + text = stringResourceSafe(R.string.news_related_tokens), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } SpacerH(12.dp) BlockCard( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt index e0d620f33b..f214105971 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt @@ -11,6 +11,7 @@ import kotlinx.collections.immutable.toPersistentList internal object MockArticlesFactory { fun createMockArticles(): ImmutableList = listOf( ArticleUM( + isTrending = false, id = 1, title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds", createdAt = TextReference.Str("20 Jun, 21:45"), @@ -78,6 +79,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 3, @@ -107,6 +109,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 4, @@ -136,6 +139,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 5, @@ -151,6 +155,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 6, @@ -167,6 +172,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 7, @@ -182,6 +188,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 8, @@ -197,6 +204,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 9, @@ -213,6 +221,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ArticleUM( id = 10, @@ -229,6 +238,7 @@ internal object MockArticlesFactory { newsUrl = "", relatedTokens = persistentListOf(), isLiked = false, + isTrending = false, ), ).toPersistentList() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt index b83bea4e2e..c9d216ab02 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt @@ -25,6 +25,7 @@ internal sealed interface ArticlesStateUM { data class LoadingError(val onRetryClicked: () -> Unit) : ArticlesStateUM } +@Immutable internal data class ArticleUM( val id: Int, val title: String, @@ -37,6 +38,7 @@ internal data class ArticleUM( val newsUrl: String, val relatedTokens: ImmutableList, val isLiked: Boolean, + val isTrending: Boolean, ) internal data class RelatedArticleUM( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index 5dd1413007..ced6feef80 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -12,13 +12,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.chip.Chip import com.tangem.core.ui.components.chip.entity.ChipUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.ds.tabs.TangemTab import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn import com.tangem.features.feed.ui.news.list.state.NewsListState @@ -29,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableSet @Composable internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + val isRedesignEnabled = LocalRedesignEnabled.current val lazyListState = rememberLazyListState() Column( @@ -44,7 +47,17 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { items = state.filters, key = { it.id }, ) { filter -> - Chip(state = filter) + if (isRedesignEnabled) { + TangemTab( + text = filter.text, + isChecked = filter.isSelected, + onCheckedChange = { + filter.onClick() + }, + ) + } else { + Chip(state = filter) + } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt index 465c76d0f9..177f191773 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.TangemBlockCardColors @@ -112,7 +112,9 @@ private fun Content( key = ArticleConfigUM::id, ) { article -> ArticleCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .heightIn(min = 152.dp) + .fillMaxWidth(), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), articleConfigUM = article, onArticleClick = { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt index 12fe9861f3..9d8c0e32b1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.feed.ui.news.list.state import androidx.compose.runtime.Immutable -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.chip.entity.ChipUM import kotlinx.collections.immutable.ImmutableList 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 3d8d7bc0bd..5b9efafb88 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 @@ -179,6 +179,9 @@ internal class UpgradeWalletModel @Inject constructor( title = resourceReference(id = R.string.alert_button_request_support), onClick = { modelScope.launch { + analyticsEventHandler.send( + Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Upgrade), + ) sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) } }, diff --git a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt index b6965fc720..a841936911 100644 --- a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt +++ b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject /** * Mocking it for release/external builds to exclude SumSub dependency - * This will never be called if the FT [isTangemPayEnabled] is off */ @Suppress("UnusedPrivateProperty") internal class MockKycComponent @AssistedInject constructor( diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index b1eb0e3830..0cf7ab210c 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) id("configuration") } diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index ca4d8956b2..fcce6daea7 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable enum class ManageTokensSource(val analyticsName: String) { STORIES(analyticsName = "Stories"), @@ -12,18 +13,15 @@ enum class ManageTokensSource(val analyticsName: String) { } sealed interface ManageTokensMode { - data class Wallet(val userWalletId: UserWalletId) : ManageTokensMode - data class Account(val accountId: AccountId) : ManageTokensMode + data class Account(val accountId: AccountId) : ManageTokensMode { + constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId)) + } data object None : ManageTokensMode } -sealed interface AddCustomTokenMode { +@Serializable +data class AddCustomTokenMode(val accountId: AccountId) { + val userWalletId: UserWalletId = accountId.userWalletId - val userWalletId: UserWalletId - - data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode - - data class Account(val accountId: AccountId) : AddCustomTokenMode { - override val userWalletId: UserWalletId = accountId.userWalletId - } + constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId)) } \ No newline at end of file diff --git a/features/manage-tokens/impl/detekt-baseline-debug.xml b/features/manage-tokens/impl/detekt-baseline-debug.xml index 6c3a7af5c8..d179bf1933 100644 --- a/features/manage-tokens/impl/detekt-baseline-debug.xml +++ b/features/manage-tokens/impl/detekt-baseline-debug.xml @@ -4,47 +4,21 @@ BooleanPropertyNaming:AddCustomTokenUM.kt$AddCustomTokenUM$val showBackButton: Boolean BooleanPropertyNaming:CustomCurrencyValidator.kt$CustomCurrencyValidator.Status.Validated$val fillForm: Boolean - BooleanPropertyNaming:CustomTokenFormModel.kt$CustomTokenFormModel$val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), ) - BooleanPropertyNaming:ManageTokensListManager.kt$ManageTokensListManager$val loadUserTokensFromRemote = when (mode) { is ManageTokensMode.Wallet, is ManageTokensMode.Account, -> source == ManageTokensSource.ONBOARDING ManageTokensMode.None, -> false } - BooleanPropertyNaming:ManageTokensModel.kt$ManageTokensModel$val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) BooleanPropertyNaming:ManageTokensUM.kt$ManageTokensUM.ManageContent$val needToInteractWithColdWallet: Boolean - BooleanPropertyNaming:OnboardingManageTokensModel.kt$OnboardingManageTokensModel$val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig$abstract val showProgress: Boolean BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig.Continue$val showTangemIcon: Boolean - MaxChainedCallsOnSameLine:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$params.initialCurrency.network.id.rawId.value MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) } - MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) } MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", placeholder = stringReference("0x1234567890"), ) } - MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to add currency") showErrorDialog() return@resource } - MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to derive public keys") showErrorDialog() return@resource } - MultilineLambdaItParameter:CustomTokenSelectorModel.kt$CustomTokenSelectorModel${ when (it) { is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() } } - MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check currency unsupported state |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) null } - MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check linked tokens |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) false } - MultilineLambdaItParameter:ManageTokensModel.kt$ManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource } - MultilineLambdaItParameter:ManageTokensUseCasesFacade.kt$ManageTokensUseCasesFacade${ it is CryptoCurrency.Token && it.network.backendId == network.backendId && it.network.derivationPath == network.derivationPath } - MultilineLambdaItParameter:OnboardingManageTokensModel.kt$OnboardingManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource } - MultilineLambdaItParameter:PreviewManageTokensComponent.kt$PreviewManageTokensComponent${ it.fastForEachIndexed { index, network -> if (index == networkIndex) { it[index] = network.copy( iconResId = if (isSelected) { R.drawable.img_eth_22 } else { R.drawable.ic_eth_16 }, isSelected = isSelected, ) } } } NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove) NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd) - NamedArguments:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$TokenSearched( params.analyticsCategoryName, token = null, blockchain = null, isTokenChosen = false, ) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$selectNetwork(currencyBatch.key, currency, networkId, isSelected) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) - NestedScopeFunctions:CustomTokenSelectorModel.kt$CustomTokenSelectorModel$let { recognizer.recognize(it) } NullableBooleanCheck:CustomCurrencyFormOperations.kt$this.tokenForm?.wasFilled ?: false - PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$bottomSheetNavigation - PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$uiState UnsafeCallOnNullableType:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider$it[Field.CONTRACT_ADDRESS]!! - UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedDerivationPath!! - UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedNetwork!! UseEmptyCounterpart:CustomTokenAnalyticsEvent.kt$CustomTokenAnalyticsEvent$mapOf() UseEmptyCounterpart:ManageTokensAnalyticEvent.kt$ManageTokensAnalyticEvent$mapOf() UseOrEmpty:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$items[currency] ?: emptySet() - UseOrEmpty:PreviewCustomTokenSelectorComponent.kt$PreviewCustomTokenSelectorComponent$d.id?.rawId?.value ?: "" - VarCouldBeVal:CustomTokenFormModel.kt$CustomTokenFormModel$private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode) 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 7c1d1b2f84..43fbc9472f 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,8 +17,6 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.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 @@ -45,7 +43,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.collections.isNotEmpty @Suppress("LongParameterList") @ModelScoped @@ -55,7 +52,6 @@ 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, @@ -63,16 +59,15 @@ internal class ChooseManagedTokensModel @Inject constructor( 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 manageTokensMode = ManageTokensMode.Account(params.userWalletId) private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory .create(mode = manageTokensMode) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow + field = MutableStateFlow(createReadContentModel()) + private val manageTokensListManager = manageTokensListManagerFactory.create( scope = modelScope, source = ManageTokensSource.SEND_VIA_SWAP, @@ -91,10 +86,6 @@ internal class ChooseManagedTokensModel @Inject constructor( }, ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val uiState: StateFlow - field = MutableStateFlow(createReadContentModel()) - init { manageTokensListManager.uiItems .onEach { items -> updateItems(items) } @@ -146,11 +137,7 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun removeNotification() { modelScope.launch { setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false) - uiState.update { - it.copy( - notificationUM = null, - ) - } + uiState.update { it.copy(notificationUM = null) } } } @@ -161,7 +148,7 @@ internal class ChooseManagedTokensModel @Inject constructor( if (!new.readContent.search.isActive && old.readContent.search.isActive) { analyticsEventHandler.send( CommonManageTokensAnalyticEvents.TokenSearched( - params.analyticsCategoryName, + categoryName = params.analyticsCategoryName, token = null, blockchain = null, isTokenChosen = false, @@ -195,8 +182,9 @@ internal class ChooseManagedTokensModel @Inject constructor( val isToken = currency.id.value == params.initialCurrency.id.rawCurrencyId?.value // Ensure that initial token network is filtered out and network list is empty + val paramsRawId = params.initialCurrency.network.id.rawId val isEmptyNetworks = availableNetworks?.networks?.filterNot { network -> - network.id == params.initialCurrency.network.id.rawId.value + network.id == paramsRawId.value }.isNullOrEmpty() // Filter out currency from display diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index bddf89e1cf..918483d121 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -16,11 +16,11 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.model.ManageTokensModel import com.tangem.features.managetokens.ui.ManageTokensScreen @@ -38,7 +38,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, - serializer = ManageTokensBottomSheetConfig.serializer(), + serializer = AccountId.serializer(), handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -57,13 +57,10 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( } private fun bottomSheetChild( - config: ManageTokensBottomSheetConfig, + accountId: AccountId, componentContext: ComponentContext, ): ComposableBottomSheetComponent { - val mode = when (config) { - is ManageTokensBottomSheetConfig.AddWalletCustomToken -> AddCustomTokenMode.Wallet(config.userWalletId) - is ManageTokensBottomSheetConfig.AddAccountCustomToken -> AddCustomTokenMode.Account(config.accountId) - } + val mode = AddCustomTokenMode(accountId) return addCustomTokenComponentFactory.create( context = childByContext(componentContext), params = AddCustomTokenComponent.Params( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index 7427de4342..b142c79097 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class PreviewAddCustomTokenComponent( initialState: AddCustomTokenConfig = AddCustomTokenConfig( - mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), + mode = AddCustomTokenMode(UserWalletId(stringValue = "321")), step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, ), ) : AddCustomTokenComponent { @@ -61,8 +61,8 @@ internal class PreviewAddCustomTokenComponent( PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( mode = config.mode, - selectedNetwork = config.selectedNetwork!!, - selectedDerivationPath = config.selectedDerivationPath!!, + selectedNetwork = requireNotNull(config.selectedNetwork), + selectedDerivationPath = requireNotNull(config.selectedDerivationPath), onDerivationPathSelected = { _, _ -> }, ), ).Content(modifier) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index bcdfa62787..ac0bd61222 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -19,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList internal class PreviewCustomTokenSelectorComponent( private val params: Params = Params.NetworkSelector( - mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), + mode = AddCustomTokenMode(UserWalletId(stringValue = "321")), selectedNetwork = null, onNetworkSelected = {}, ), @@ -39,7 +39,7 @@ internal class PreviewCustomTokenSelectorComponent( ) DerivationPathUM( - id = d.id?.rawId?.value ?: "", + id = d.id?.rawId?.value.orEmpty(), value = d.value.value.orEmpty(), networkName = stringReference(d.name), isSelected = d.value == params.selectedDerivationPath?.value, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index bf0778fc87..ed67875a8e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -40,9 +40,7 @@ internal class PreviewManageTokensComponent( popBack = {}, items = items, topBar = when (params.mode) { - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> ManageTokensTopBarUM.ManageContent( + is ManageTokensMode.Account -> ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = {}, endButton = TopAppBarButtonUM.Icon( @@ -206,10 +204,10 @@ internal class PreviewManageTokensComponent( is CurrencyItemUM.Basic -> { val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) ?.copy( - networks = item.networks.networks.toPersistentList().mutate { - it.fastForEachIndexed { index, network -> + networks = item.networks.networks.toPersistentList().mutate { networks -> + networks.fastForEachIndexed { index, network -> if (index == networkIndex) { - it[index] = network.copy( + networks[index] = network.copy( iconResId = if (isSelected) { R.drawable.img_eth_22 } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt deleted file mode 100644 index 91a4b7fba3..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.managetokens.entity.managetokens - -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class ManageTokensBottomSheetConfig { - - @Serializable - data class AddWalletCustomToken( - val userWalletId: UserWalletId, - ) : ManageTokensBottomSheetConfig() - - @Serializable - data class AddAccountCustomToken( - val accountId: AccountId, - ) : ManageTokensBottomSheetConfig() -} \ No newline at end of file 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 d741fc5ed0..5418e2b7b1 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 @@ -181,7 +181,7 @@ internal class CustomTokenFormModel @Inject constructor( isAlreadyAdded: Boolean, isCustom: Boolean, ) = modelScope.launch { - val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( + val isNeedColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), ) @@ -195,7 +195,7 @@ internal class CustomTokenFormModel @Inject constructor( clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, - walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { needColdWalletInteraction }, + walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { isNeedColdWalletInteraction }, ) if (fillForm) { @@ -355,11 +355,6 @@ internal class CustomTokenFormModel @Inject constructor( 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 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 7aa77b2a2b..731f1757df 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 @@ -15,7 +15,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.managetokens.GetSupportedNetworksUseCase import com.tangem.domain.models.account.Account @@ -52,7 +51,6 @@ internal class CustomTokenSelectorModel @Inject constructor( private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, private val messageSender: UiMessageSender, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model() { @@ -171,7 +169,7 @@ internal class CustomTokenSelectorModel @Inject constructor( } private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List { - return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> + return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { _ -> val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) messageSender.send(message) @@ -194,19 +192,23 @@ internal class CustomTokenSelectorModel @Inject constructor( fun selectCustomDerivationPath(value: SelectedDerivationPath) { when (params) { is NetworkSelector -> return - is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) { - params.checkAccountDerivation(value) - } else { - params.onDerivationPathSelected(value, null) - } + is DerivationPathSelector -> params.checkAccountDerivation(value) } } private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) = modelScope.launch { val account = derivationPath.id - ?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer) - ?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } } + ?.let { Blockchain.fromId(it.rawId.value) } + ?.let(::AccountNodeRecognizer) + ?.let { recognizer -> + val derivationPathValue = derivationPath.value.value + if (derivationPathValue != null) { + recognizer.recognize(derivationPathValue) + } else { + null + } + } ?.let { accountNode -> fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount && this.account.derivationIndex.value.toLong() == accountNode 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 7c5611d12e..960347fc65 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 @@ -18,12 +18,12 @@ 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.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.account.AccountId import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.entity.item.CurrencyItemUM -import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R @@ -66,7 +66,7 @@ internal class ManageTokensModel @Inject constructor( ) val state: MutableStateFlow = MutableStateFlow(getInitialState()) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { manageTokensListManager.uiItems @@ -101,15 +101,12 @@ internal class ManageTokensModel @Inject constructor( analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source)) return when (params.mode) { - is ManageTokensMode.Wallet, - is ManageTokensMode.Account, - -> createManageContentModel() + is ManageTokensMode.Account -> createManageContentModel() ManageTokensMode.None -> createReadContentModel() } } private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) { - is ManageTokensMode.Wallet -> manageContentTopBar() is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -196,9 +193,7 @@ internal class ManageTokensModel @Inject constructor( state.update { it.copySealed(topBar = manageContentTopBar()) } } } - ManageTokensMode.None, - is ManageTokensMode.Wallet, - -> Unit // use init state + ManageTokensMode.None -> Unit // use init state } } @@ -299,12 +294,12 @@ internal class ManageTokensModel @Inject constructor( .flatten() .toSet() .associate { network -> network.backendId to network.derivationPath.value } - val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) + val isNeedToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) state.update { state -> state.copySealed( hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), - needToInteractWithColdWallet = needToInteractWithColdWallet, + needToInteractWithColdWallet = isNeedToInteractWithColdWallet, ) } } @@ -324,12 +319,8 @@ internal class ManageTokensModel @Inject constructor( private fun navigateToAddCustomToken() { analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source)) when (val portfolio = params.mode) { - is ManageTokensMode.Wallet -> - bottomSheetNavigation - .activate(ManageTokensBottomSheetConfig.AddWalletCustomToken(portfolio.userWalletId)) is ManageTokensMode.Account -> - bottomSheetNavigation - .activate(ManageTokensBottomSheetConfig.AddAccountCustomToken(portfolio.accountId)) + bottomSheetNavigation.activate(portfolio.accountId) ManageTokensMode.None -> Unit } } @@ -347,8 +338,8 @@ internal class ManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index e2aaa97cca..02a234043f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -13,8 +13,6 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.account.AccountId import com.tangem.domain.redux.OnboardingManageTokensAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent @@ -45,18 +43,13 @@ internal class OnboardingManageTokensModel @Inject constructor( private val messageSender: UiMessageSender, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventHandler: AnalyticsEventHandler, - accountsFeatureToggles: AccountsFeatureToggles, manageTokensListManagerFactory: ManageTokensListManager.Factory, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params: OnboardingManageTokensComponent.Params = paramsContainer.require() - private val portfolio = if (accountsFeatureToggles.isFeatureEnabled) { - ManageTokensMode.Account(accountId = AccountId.forMainCryptoPortfolio(params.userWalletId)) - } else { - ManageTokensMode.Wallet(params.userWalletId) - } + private val portfolio = ManageTokensMode.Account(params.userWalletId) private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory .create(mode = portfolio) private val manageTokensListManager = manageTokensListManagerFactory.create( @@ -223,12 +216,12 @@ internal class OnboardingManageTokensModel @Inject constructor( .flatten() .toSet() .associate { network -> network.backendId to network.derivationPath.value } - val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) + val shouldShowTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) state.update { state -> state.copy( actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue( onClick = ::saveChanges, - showTangemIcon = showTangemIcon, + showTangemIcon = shouldShowTangemIcon, ), ) } @@ -267,8 +260,8 @@ internal class OnboardingManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } @@ -292,8 +285,8 @@ internal class OnboardingManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index 4699454540..9665917bbb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -69,7 +69,7 @@ private fun Preview_AddCustomTokenBottomSheet( } private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider { - private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) + private val mode: AddCustomTokenMode get() = AddCustomTokenMode(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( PreviewAddCustomTokenComponent(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 29cb995a99..4922a66af7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -269,7 +269,7 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewParameterProvider { private val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0") - private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) + private val mode: AddCustomTokenMode get() = AddCustomTokenMode(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 2b20edf084..82579b41d9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -443,7 +443,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider { - return if (accountsFeatureToggles.isFeatureEnabled) { - either { - val accountId = getAccountId(currency) + return either { + val accountId = getAccountId(currency) - manageCryptoCurrenciesUseCase( - accountId = accountId, - add = currency, - skipDerivationErrors = false, - ).bind() - } - } else { - addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency) - } - } - - suspend fun derivePublicKeysUseCase(currencies: List): Either { - return if (accountsFeatureToggles.isFeatureEnabled) { - Unit.right() - } else { - derivePublicKeysUseCase.invoke(userWalletId = userWalletId, currencies = currencies) + manageCryptoCurrenciesUseCase( + accountId = accountId, + add = currency, + skipDerivationErrors = false, + ).bind() } } @@ -67,23 +46,14 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( networkId: Network.ID, derivationPath: Network.DerivationPath, contractAddress: String?, - ): Either = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - networkId = networkId, - derivationPath = derivationPath, - contractAddress = contractAddress, - ) - .fold(ifEmpty = { true }, ifSome = { false }) - .right() - } else { - checkIsCurrencyNotAddedUseCase.invoke( - userWalletId = userWalletId, - networkId = networkId, - derivationPath = derivationPath, - contractAddress = contractAddress, - ) - } + ): Either = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + contractAddress = contractAddress, + ) + .fold(ifEmpty = { true }, ifSome = { false }) + .right() private suspend fun Raise.getAccountId(currency: CryptoCurrency): AccountId { val accountList = singleAccountListSupplier.getSyncOrNull( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index d0221350ce..6265318696 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -89,12 +89,9 @@ internal class ManageTokensListManager @AssistedInject constructor( * @param isCollapsed set initial display state of networks. !!! WARNING !!! Use `false` flag with cation */ suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope { - val loadUserTokensFromRemote = when (mode) { - is ManageTokensMode.Wallet, - is ManageTokensMode.Account, - -> source == ManageTokensSource.ONBOARDING - ManageTokensMode.None, - -> false + val shouldLoadTokensFromRemote = when (mode) { + is ManageTokensMode.Account -> source == ManageTokensSource.ONBOARDING + ManageTokensMode.None -> false } val batchFlow = useCasesFacade.getManagedTokensUseCase( context = ManageTokensListBatchingContext( @@ -102,7 +99,7 @@ internal class ManageTokensListManager @AssistedInject constructor( coroutineScope = this, ), // only for onboarding case, change carefully and check repository implementation - loadUserTokensFromRemote = loadUserTokensFromRemote, + loadUserTokensFromRemote = shouldLoadTokensFromRemote, ) batchFlow.state @@ -185,9 +182,7 @@ internal class ManageTokensListManager @AssistedInject constructor( } val canEditItems = when (state.mode) { - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> true + is ManageTokensMode.Account -> true ManageTokensMode.None -> false } state.copy( @@ -210,7 +205,7 @@ internal class ManageTokensListManager @AssistedInject constructor( override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { changedCurrenciesManager.addCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) + sendSelectCurrencyAction(batchKey = batchKey, currencyId = currency.id, network = network, isSelected = true) sendSelectCurrencyAnalyticsEvent(currency, isSelected = true) } @@ -218,7 +213,7 @@ internal class ManageTokensListManager @AssistedInject constructor( override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { changedCurrenciesManager.removeCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) + sendSelectCurrencyAction(batchKey = batchKey, currencyId = currency.id, network = network, isSelected = false) sendSelectCurrencyAnalyticsEvent(currency, isSelected = false) } @@ -270,9 +265,9 @@ internal class ManageTokensListManager @AssistedInject constructor( network = network, tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, - ).getOrElse { + ).getOrElse { throwable -> Timber.e( - it, + throwable, """ Failed to check linked tokens |- Mode: $mode @@ -281,7 +276,7 @@ internal class ManageTokensListManager @AssistedInject constructor( ) val message = SnackbarMessage( - message = it.localizedMessage + message = throwable.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) @@ -296,9 +291,9 @@ internal class ManageTokensListManager @AssistedInject constructor( ): CurrencyUnsupportedState? { return useCasesFacade.checkCurrencyUnsupportedUseCase( sourceNetwork = sourceNetwork, - ).getOrElse { + ).getOrElse { throwable -> Timber.e( - it, + throwable, """ Failed to check currency unsupported state |- Mode: $mode @@ -307,7 +302,7 @@ internal class ManageTokensListManager @AssistedInject constructor( ) val message = SnackbarMessage( - message = it.localizedMessage + message = throwable.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) @@ -334,7 +329,12 @@ internal class ManageTokensListManager @AssistedInject constructor( toRemove = currenciesToRemove.value, ), onSelectCurrencyNetwork = { networkId, isSelected -> - selectNetwork(currencyBatch.key, currency, networkId, isSelected) + selectNetwork( + batchKey = currencyBatch.key, + currency = currency, + source = networkId, + isSelected = isSelected, + ) }, onLongTap = ::copyContractAddress, ) 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 e692352aad..63cab69a36 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 @@ -3,11 +3,12 @@ package com.tangem.features.managetokens.utils.list import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles 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.managetokens.* +import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase +import com.tangem.domain.managetokens.GetDistinctManagedCurrenciesUseCase +import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency @@ -26,14 +27,10 @@ import dagger.assisted.AssistedInject internal class ManageTokensUseCasesFacade @AssistedInject constructor( val getManagedTokensUseCase: GetManagedTokensUseCase, val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, - private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, - private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveManagedTokensUseCase: SaveManagedTokensUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val customTokensRepository: CustomTokensRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, private val singleAccountSupplier: SingleAccountSupplier, @Assisted private val mode: ManageTokensMode, ) { @@ -44,17 +41,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( fun manageTokensListConfig(searchText: String?): ManageTokensListConfig { return when (mode) { is ManageTokensMode.Account -> { - ManageTokensListConfig.Account(accountId = mode.accountId, searchText = searchText) - } - is ManageTokensMode.Wallet -> { - ManageTokensListConfig.Wallet(userWalletId = mode.userWalletId, searchText = searchText) + ManageTokensListConfig(accountId = mode.accountId, searchText = searchText) } ManageTokensMode.None -> { - if (accountsFeatureToggles.isFeatureEnabled) { - ManageTokensListConfig.Account(accountId = null, searchText = searchText) - } else { - ManageTokensListConfig.Wallet(userWalletId = null, searchText = searchText) - } + ManageTokensListConfig(accountId = null, searchText = searchText) } } } @@ -69,10 +59,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( manageCryptoCurrenciesUseCase(accountId = mode.accountId, remove = currency) } - is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke( - userWalletId = mode.userWalletId, - customCurrency = customCurrency, - ) ManageTokensMode.None -> nonePortfolioError.left() } } @@ -92,18 +78,12 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( ) as? Account.CryptoPortfolio ?: return IllegalStateException("Account not found").left() - (account.cryptoCurrencies + added - removed).any { - it is CryptoCurrency.Token && it.network.backendId == network.backendId && - it.network.derivationPath == network.derivationPath + (account.cryptoCurrencies + added - removed).any { currency -> + currency is CryptoCurrency.Token && currency.network.backendId == network.backendId && + currency.network.derivationPath == network.derivationPath } .right() } - is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke( - userWalletId = mode.userWalletId, - network = network, - tempAddedTokens = tempAddedTokens, - tempRemovedTokens = tempRemovedTokens, - ) ManageTokensMode.None -> nonePortfolioError.left() } } @@ -116,10 +96,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( userWalletId = mode.accountId.userWalletId, sourceNetwork = sourceNetwork, ) - is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke( - userWalletId = mode.userWalletId, - sourceNetwork = sourceNetwork, - ) ManageTokensMode.None -> nonePortfolioError.left() } } @@ -129,10 +105,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( userWalletId = mode.accountId.userWalletId, networksWithDerivationPath = network, ) - is ManageTokensMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = mode.userWalletId, - networksWithDerivationPath = network, - ) ManageTokensMode.None -> false } @@ -147,13 +119,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( remove = currenciesToRemove.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId), ) } - is ManageTokensMode.Wallet -> { - saveManagedTokensUseCase.invoke( - userWalletId = mode.userWalletId, - currenciesToAdd = currenciesToAdd, - currenciesToRemove = currenciesToRemove, - ) - } ManageTokensMode.None -> nonePortfolioError.left() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt index cbc53ded2c..146f16ebbc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt @@ -29,9 +29,7 @@ internal class ManageTokensWarningDelegate @AssistedInject constructor( ) { val isNonePortfolio = when (mode) { ManageTokensMode.None -> true - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> false + is ManageTokensMode.Account -> false } val hasLinkedTokens = if (isNonePortfolio || !isCoin) { false diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt deleted file mode 100644 index 0136e2532f..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.features.markets.details - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsTokenDetailsComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val appCurrency: AppCurrency, - val shouldShowPortfolio: Boolean, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val blockchain: String?, - val source: String, - ) - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt deleted file mode 100644 index 05a7e68670..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.markets.entry - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState - -@Stable -interface MarketsEntryComponent { - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory { - fun create(context: AppComponentContext): MarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 1928a996ed..86304b9fd5 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.staking) @@ -49,12 +50,6 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt deleted file mode 100644 index e6f564100f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.features.markets.details.impl - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.child -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@Stable -internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: Params, - analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - portfolioComponentFactory: MarketsPortfolioComponent.Factory, -) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { - - // applying l2 compatibility - private val updatedParams = params.copy( - token = params.token.copy( - id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)), - ), - ) - private val analyticsParams = params.analyticsParams - - private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) - - private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) { - portfolioComponentFactory.create( - context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params( - updatedParams.token, - analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) }, - ), - ) - } else { - null - } - - init { - componentScope.launch { - model.networksState.collectLatest { networksState -> - when (networksState) { - is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks( - networksState.networks, - ) - TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable() - else -> {} - } - } - } - - // === Analytics === - if (analyticsParams != null) { - analyticsEventHandler.send( - MarketDetailsAnalyticsEvent.EventBuilder( - token = params.token, - ).screenOpened( - blockchain = analyticsParams.blockchain, - source = analyticsParams.source, - ), - ) - } - } - - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - val bsState by bottomSheetState - - LaunchedEffect(bsState) { - model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED - } - - BackHandler(enabled = bsState == BottomSheetState.EXPANDED) { - navigateBack() - } - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = LocalMainBottomSheetColor.current.value, - addTopBarStatusBarPadding = false, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = bsState == BottomSheetState.EXPANDED, - onHeaderSizeChange = onHeaderSizeChange, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - @Composable - override fun Content(modifier: Modifier) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = TangemTheme.colors.background.tertiary, - addTopBarStatusBarPadding = true, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = true, - onHeaderSizeChange = {}, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - private fun navigateBack() = router.pop() - - @AssistedFactory - interface Factory : MarketsTokenDetailsComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt deleted file mode 100644 index c3674eb4cc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketParams - -internal class MarketDetailsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - ) { - fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent( - event = "Token Chart Screen Opened", - params = buildMap { - put("Token", token.symbol) - blockchain?.let { put("blockchain", it) } - put("Source", source) - }, - ) - - fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent( - event = "Button - Period", - params = mapOf( - "Token" to token.symbol, - "Period" to interval.toAnalyticsString(), - "Source" to intervalType.source, - ), - ) - - fun readMoreClicked() = MarketDetailsAnalyticsEvent( - event = "Button - Read More", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent( - event = "Button - Links", - params = mapOf( - "Token" to token.symbol, - "Link" to linkTitle, - ), - ) - - fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent( - event = "Exchanges Screen Opened", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun securityScoreOpened() = MarketDetailsAnalyticsEvent( - event = "Security Score Info", - params = mapOf("Token" to token.symbol), - ) - - fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent( - event = "Security Score Provider Clicked", - params = mapOf( - "Token" to token.symbol, - "Provider" to provider, - ), - ) - } - - enum class IntervalType(val source: String) { - Chart("Chart"), - PricePerformance("Price"), - Insights("Insights"), - } -} - -private fun PriceChangeInterval.toAnalyticsString() = when (this) { - PriceChangeInterval.H24 -> "24h" - PriceChangeInterval.WEEK -> "7d" - PriceChangeInterval.MONTH -> "1m" - PriceChangeInterval.MONTH3 -> "3m" - PriceChangeInterval.MONTH6 -> "6m" - PriceChangeInterval.YEAR -> "1y" - PriceChangeInterval.ALL_TIME -> "All" -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt deleted file mode 100644 index 8ef6de84d7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent -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 bindMarketsTokenDetailsComponent( - factory: DefaultMarketsTokenDetailsComponent.Factory, - ): MarketsTokenDetailsComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt deleted file mode 100644 index 2fda5dee58..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -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(MarketsTokenDetailsModel::class) - fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt deleted file mode 100644 index f2a44120b9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ /dev/null @@ -1,645 +0,0 @@ -package com.tangem.features.markets.details.impl.model - -import androidx.compose.runtime.Stable -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.sorted -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.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -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.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains -import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.markets.* -import com.tangem.domain.models.wallet.UserWallet -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.usecase.GetWalletsUseCase -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter -import com.tangem.features.markets.details.impl.model.converters.ExchangeItemStateConverter -import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import org.joda.time.DateTime -import java.math.BigDecimal -import java.util.Locale -import javax.inject.Inject - -@Suppress("LargeClass", "LongParameterList") -@Stable -@ModelScoped -internal class MarketsTokenDetailsModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, - private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, - private val getTokenExchangesUseCase: GetTokenExchangesUseCase, - private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserCountryUseCase: GetUserCountryUseCase, - private val getUserWalletsUseCase: GetWalletsUseCase, -) : Model() { - - private val quotesJob = JobHolder() - private var userCountry: UserCountry? = null - private val params = paramsContainer.require() - private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token) - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = params.appCurrency, - ) - - private val infoConverter = TokenMarketInfoConverter( - appCurrency = Provider { currentAppCurrency.value }, - onInfoClick = { showBottomSheet(it) }, - onListedOnClick = ::onListedOnClick, - onLinkClick = { link -> - urlOpener.openUrl(link.url) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title)) - }, - onSecurityScoreInfoClick = { content -> - showBottomSheet(content) - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened()) - }, - onSecurityScoreProviderLinkClick = { provider -> - provider.urlData?.fullUrl?.let { url -> - urlOpener.openUrl(url) - } - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(provider.name)) - }, - // === Analytics === - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - onInsightsIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights, - interval = interval, - ), - ) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - // ================== - ) - - private val descriptionConverter = DescriptionConverter( - onReadModeClicked = { content -> - showBottomSheet(content) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked()) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - onGeneratedAINotificationClick = { - modelScope.launch { - sendFeedbackEmailUseCase( - type = FeedbackEmailType.CurrencyDescriptionError( - currencyId = params.token.id.value, - currencyName = params.token.name, - ), - ) - } - }, - ) - - private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { - chartData = MarketChartData.NoData.Loading - - updateLook { currentLook -> - val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType() - - currentLook.copy( - type = percentChangeType.toChartType(), - xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), - yAxisFormatter = { value -> - value.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - } - }, - ) - } - } - - private val currentQuotes = MutableStateFlow( - TokenQuotes( - currentPrice = params.token.tokenQuotes.currentPrice, - h24ChangePercent = params.token.tokenQuotes.h24Percent, - weekChangePercent = params.token.tokenQuotes.weekPercent, - monthChangePercent = params.token.tokenQuotes.monthPercent, - m3ChangePercent = null, - m6ChangePercent = null, - yearChangePercent = null, - allTimeChangePercent = null, - ), - ) - - private val currentTokenInfo = MutableStateFlow(null) - private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) - - val isVisibleOnScreen = MutableStateFlow(false) - val networksState = MutableStateFlow(TokenNetworksState.Loading) - - val state = MutableStateFlow( - MarketsTokenDetailsUM( - tokenName = params.token.name, - priceText = params.token.tokenQuotes.currentPrice.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - }, - dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, - priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), - iconUrl = params.token.imageUrl, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = chartDataProducer, - onLoadRetryClick = ::onLoadRetryClicked, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = ::onMarkerPointSelected, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = ::onSelectedIntervalChange, - isMarkerSet = false, - body = MarketsTokenDetailsUM.Body.Loading, - triggerPriceChange = consumedEvent(), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - ), - ) - - private val quotesStateUpdater = QuotesStateUpdater( - currentAppCurrency = Provider { currentAppCurrency.value }, - state = state, - currentQuotes = currentQuotes, - lastUpdatedTimestamp = lastUpdatedTimestamp, - currentTokenInfo = currentTokenInfo, - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - ) - - private val loadChartJobHolder = JobHolder() - - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) - // reload screen if currency changed - modelScope.launch { - currentAppCurrency - .filter { it != params.appCurrency } - .collectLatest { _ -> - initialLoad() - } - } - - initialLoad() - } - - private fun initialLoad() { - loadInfo() - loadChart(state.value.selectedInterval) - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - - private fun loadQuotes() { - modelScope.launch { - val result = getTokenFullQuotesUseCase( - tokenId = params.token.id, - appCurrency = currentAppCurrency.value, - tokenSymbol = params.token.symbol, - ) - - result.onRight { res -> - updateQuotes(res) - } - } - } - - private fun loadChart(interval: PriceChangeInterval) { - modelScope.launch { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.NoData.Loading - } - - val chart = getTokenPriceChartUseCase.invoke( - appCurrency = currentAppCurrency.value, - interval = interval, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - preview = false, - ) - - state.update { currentState -> - currentState.copy( - selectedInterval = interval, - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chart - .onRight { updateTokenChart(it) } - .onLeft { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.ERROR, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Error) { - MarketsTokenDetailsUM.Body.Nothing - } else { - currentState.body - }, - ) - } - } - }.saveIn(loadChartJobHolder) - } - - private suspend fun updateTokenChart(tokenChart: TokenChart) { - val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.Data( - y = tokenChart.priceY.toImmutableList(), - x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ).sorted() - - updateLook { currentLook -> - currentLook.copy( - xAxisFormatter = xAxisFormatter, - type = state.value.priceChangeType.toChartType(), - ) - } - } - - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.DATA, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Nothing) { - MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) - } else { - currentState.body - }, - ) - } - } - - private fun loadInfo() { - state.update { currentState -> - currentState.copy( - body = MarketsTokenDetailsUM.Body.Loading, - ) - } - - modelScope.launch { - val tokenMarketInfo = getTokenMarketInfoUseCase( - appCurrency = currentAppCurrency.value, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - ) - - tokenMarketInfo.fold( - ifRight = { result -> updateInfo(result) }, - ifLeft = { - state.update { currentState -> - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Error( - onLoadRetryClick = ::onLoadRetryClicked, - ), - ) - } else { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Nothing, - ) - } - } - }, - ) - } - } - - private fun updateInfo(newInfo: TokenMarketInfo) { - lastUpdatedTimestamp.value = DateTime.now().millis - - currentTokenInfo.value = newInfo - currentQuotes.value = newInfo.quotes - - val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) - - state.update { currentState -> - currentState.copy( - priceText = newInfo.quotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - }, - priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( - interval = currentState.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(newInfo), - infoBlocks = infoConverter.convert(newInfo), - ), - ) - } - - val areAllWalletsHot = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = newInfo.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = areAllWalletsHot, - ) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes) - - quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes) - - val percent = populatedNewQuotes - .getPercentByInterval(interval = state.value.selectedInterval) - - chartDataProducer.runTransaction { - updateLook { - it.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private fun onSelectedIntervalChange(interval: PriceChangeInterval) { - if (state.value.selectedInterval == interval) return - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart, - interval = interval, - ), - ) - // ================== - - val quotes = currentQuotes.value - val priceChangePercent = quotes.getFormattedPercentByInterval(interval) - - state.update { currentState -> - currentState.copy( - priceChangePercentText = priceChangePercent, - selectedInterval = interval, - priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType() - ?: PriceChangeType.NEUTRAL, - dateTimeText = getDefaultDateTimeString(interval), - ) - } - - loadChart(interval) - - if (priceChangePercent.isEmpty()) { - loadQuotes() - } - } - - @Suppress("MagicNumber") - private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) { - val currentState = state.value - - val dateTimeText = markerTimestamp?.let { timestamp -> - MarketsDateTimeFormatters.formatDateByIntervalWithMarker( - interval = currentState.selectedInterval, - markerTimestamp = timestamp, - ) - } ?: getDefaultDateTimeString(currentState.selectedInterval) - - val priceText = (price ?: currentQuotes.value.currentPrice).format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - } - - val percent = price?.let { selectedPrice -> - getChangePercentBetween( - previousPrice = selectedPrice, - currentPrice = currentQuotes.value.currentPrice, - ) - } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) - - val percentText = percent?.format { percent() }.orEmpty() - - state.update { stateToUpdate -> - stateToUpdate.copy( - isMarkerSet = markerTimestamp != null, - dateTimeText = dateTimeText, - priceText = priceText, - priceChangePercentText = percentText, - priceChangeType = percent.percentChangeType(), - ) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy( - type = percent.percentChangeType().toChartType(), - ) - } - } - } - - private fun showBottomSheet(content: TangemBottomSheetConfigContent) { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy( - isShown = true, - onDismissRequest = ::hideBottomSheet, - content = content, - ), - ) - } - } - - private fun hideBottomSheet() { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false), - ) - } - } - - private fun onLoadRetryClicked() { - val currentState = state.value - - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) { - loadChart(currentState.selectedInterval) - } - - if (currentState.body is MarketsTokenDetailsUM.Body.Error || - currentState.body is MarketsTokenDetailsUM.Body.Nothing - ) { - loadInfo() - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - } - - private fun onListedOnClick(exchangesCount: Int) { - modelScope.launch { - analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) - - showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount)) - - val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id) - - // Delay to show the bottom sheet - delay(timeMillis = 400L) - - updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount) - } - } - - private fun updateExchangeBSContent( - maybeExchanges: Either>, - exchangesCount: Int, - ) { - val content = maybeExchanges - .fold( - ifLeft = { - ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) }) - }, - ifRight = { exchanges -> - ExchangesBottomSheetContent.Content( - exchangeItems = ExchangeItemStateConverter.convertList(exchanges).toImmutableList(), - ) - }, - ) - - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content), - ) - } - } - - private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { - launch { - while (true) { - delay(timeMillis) - // Update quotes only when content is visible on the screen - isVisibleOnScreen.first { it } - - loadQuotes() - } - }.saveIn(quotesJob) - } - - private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference { - return MarketsDateTimeFormatters.formatDateByInterval( - interval = interval, - startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( - interval = interval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - ) - } - - private companion object { - const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt deleted file mode 100644 index ae495c8db0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -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.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -internal class DescriptionConverter( - private val onReadModeClicked: (InfoBottomSheetContent) -> Unit, - private val onGeneratedAINotificationClick: () -> Unit, - private val needApplyFCARestrictions: Provider, -) : Converter { - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? { - if (needApplyFCARestrictions()) return null - val shortDesc = value.shortDescription ?: return null - return MarketsTokenDetailsUM.Description( - shortDescription = stringReference(shortDesc), - fullDescription = value.fullDescription?.let(::stringReference), - onReadMoreClick = { - onReadModeClicked( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_about_token_title, - wrappedList( - value.name, - ), - ), - body = stringReference(value.fullDescription.orEmpty()), - generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM( - onClick = onGeneratedAINotificationClick, - ), - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt deleted file mode 100644 index 096f0d0430..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.markets.TokenMarketExchange -import com.tangem.domain.markets.TokenMarketExchange.TrustScore -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketExchange] to [TokenItemState] - * -[REDACTED_AUTHOR] - */ -internal object ExchangeItemStateConverter : Converter { - - override fun convert(value: TokenMarketExchange): TokenItemState { - return TokenItemState.Content( - id = value.id, - iconState = CurrencyIconState.CoinIcon( - url = value.imageUrl, - fallbackResId = R.drawable.ic_alert_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content( - text = value.volumeInUsd.format { - fiat( - fiatCurrencyCode = "USD", - fiatCurrencySymbol = "$", - ).price() - }, - ), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"), - ), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = value.trustScore.toAuditLabelUM(), - ), - onItemClick = null, - onItemLongClick = null, - ) - } - - private fun TrustScore.toAuditLabelUM(): AuditLabelUM { - return when (this) { - TrustScore.Risky -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky), - type = AuditLabelUM.Type.Prohibition, - ) - TrustScore.Caution -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution), - type = AuditLabelUM.Type.Warning, - ) - TrustScore.Trusted -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted), - type = AuditLabelUM.Type.Permit, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt deleted file mode 100644 index 65273ee0bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -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.rawCompact -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal - -@Stable -internal class InsightsConverter( - private val appCurrency: Provider, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Insights): InsightsUM { - return with(value) { - InsightsUM( - h24Info = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.day, - holdersChange = holdersChange?.day, - liquidityChange = liquidityChange?.day, - buyPressureChange = buyPressureChange?.day, - ), - weekInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.week, - holdersChange = holdersChange?.week, - liquidityChange = liquidityChange?.week, - buyPressureChange = buyPressureChange?.week, - ), - monthInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.month, - holdersChange = holdersChange?.month, - liquidityChange = liquidityChange?.month, - buyPressureChange = buyPressureChange?.month, - ), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_insights), - body = resourceReference( - R.string.markets_insights_info_description_message, - wrappedList(value.sourceNetworks.joinToString { it.name }), - ), - ), - ) - }, - onIntervalChanged = onIntervalChanged, - ) - } - } - - private fun createInfoPointList( - experiencedBuyerChange: BigDecimal?, - holdersChange: BigDecimal?, - liquidityChange: BigDecimal?, - buyPressureChange: BigDecimal?, - ): ImmutableList { - return listOfNotNull( - experiencedBuyerChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers_full), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ) - }, - buyPressureChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure_full), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ) - }, - holdersChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders_full), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ) - }, - liquidityChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity_full), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ) - }, - ).toImmutableList() - } - - private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { - return when { - this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP - this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN - else -> null - } - } - - private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { - val value = if (isFiatValue) { - this.abs().format { - val currency = appCurrency() - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } else { - this.abs().format { - rawCompact() - } - } - - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + value - this == BigDecimal.ZERO -> value - else -> StringsSigns.DASH_SIGN - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt deleted file mode 100644 index ca676ee2c5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -@Stable -internal class LinksConverter( - private val onLinkClick: (LinksUM.Link) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Links): LinksUM { - return LinksUM( - officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(), - social = value.social?.map { it.convert() }.orEmpty().toImmutableList(), - repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(), - blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(), - onLinkClick = onLinkClick, - ) - } - - private fun TokenMarketInfo.Link.convert(): LinksUM.Link { - return LinksUM.Link( - title = title, - iconRes = getIconById(id), - url = link, - ) - } - - private fun getIconById(id: String?): Int { - return when (id) { - "linkedin" -> R.drawable.ic_linkedin_24 - "discord" -> R.drawable.ic_discord_24 - "youtube" -> R.drawable.ic_youtube_24 - "telegram" -> R.drawable.ic_telegram_24 - "github" -> R.drawable.ic_github_24 - "twitter" -> R.drawable.ic_twitter_24 - "facebook" -> R.drawable.ic_facebook_24 - "reddit" -> R.drawable.ic_reddit_24 - "instagram" -> R.drawable.ic_instagram_24 - "whitepaper" -> R.drawable.ic_doc_24 - else -> R.drawable.ic_arrow_top_right_24 - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt deleted file mode 100644 index c91039839b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.format.bigdecimal.compact -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.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -@Stable -internal class MetricsConverter( - private val appCurrency: Provider, - private val tokenSymbol: String, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, -) : Converter { - - @Suppress("LongMethod") - override fun convert(value: TokenMarketInfo.Metrics): MetricsUM { - return with(value) { - MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = marketCap.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_market_capitalization_full, - ), - body = resourceReference( - R.string.markets_token_details_market_capitalization_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = marketRating?.toString() ?: StringsSigns.DASH_SIGN, - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_market_rating_full), - body = resourceReference(R.string.markets_token_details_market_rating_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = volume24h.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_trading_volume_full), - body = resourceReference( - R.string.markets_token_details_trading_volume_24h_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = fullyDilutedValuation.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_full, - ), - body = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = circulatingSupply.formatAmount(crypto = true), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_circulating_supply_full), - body = resourceReference( - R.string.markets_token_details_circulating_supply_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_max_supply), - value = maxSupply.formatMaxSupply(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_max_supply_full), - body = resourceReference(R.string.markets_token_details_total_supply_description), - ), - ) - }, - ), - ), - ) - } - } - - private fun BigDecimal?.formatMaxSupply(): String { - when (this) { - null -> return StringsSigns.DASH_SIGN - BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN - } - - return this.formatAmount(crypto = true) - } - - private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - - return if (crypto) { - format { - crypto( - symbol = tokenSymbol, - decimals = 2, - ).compact() - } - } else { - val currency = appCurrency() - - format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt deleted file mode 100644 index c3aa321cb1..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import java.math.BigDecimal -import java.math.RoundingMode - -@Stable -internal class PricePerformanceConverter( - private val appCurrency: Provider, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - - fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { - return PricePerformanceUM( - h24 = value.day.convert(currentPrice), - month = value.month.convert(currentPrice), - all = value.allTime.convert(currentPrice), - onIntervalChanged = onIntervalChanged, - ) - } - - private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { - if (this == null || this.low == null || this.high == null) { - return PricePerformanceUM.Value( - low = StringsSigns.DASH_SIGN, - high = StringsSigns.DASH_SIGN, - indicatorFraction = 0f, - ) - } - - return PricePerformanceUM.Value( - low = low.convert(), - high = high.convert(), - indicatorFraction = calculateFraction(currentPrice), - ) - } - - private fun BigDecimal?.convert(): String { - val currency = appCurrency() - - return format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).price() - } - } - - private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { - val lowValue = low - val highValue = high - return when { - lowValue == null || highValue == null || highValue == BigDecimal.ZERO || currentPrice < lowValue -> 0f - currentPrice > highValue || lowValue == highValue -> 1f - else -> { - (currentPrice - lowValue).divide(highValue - lowValue, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt deleted file mode 100644 index d3bec1f927..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.model.formatter.MarketsDateTimeFormatters -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -@Stable -internal class SecurityScoreConverter( - private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM { - val ratingsCount = value.securityScoreProviderData.size - return SecurityScoreUM( - score = value.totalSecurityScore, - description = pluralReference( - id = R.plurals.markets_token_details_based_on_ratings, - count = ratingsCount, - formatArgs = wrappedList(ratingsCount), - ), - onInfoClick = { - onSecurityScoreInfoClick( - SecurityScoreBottomSheetContent( - title = resourceReference(R.string.markets_token_details_security_score), - description = resourceReference(R.string.markets_token_details_security_score_description), - providers = value.securityScoreProviderData.map { provider -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = provider.providerName, - lastAuditDate = provider.lastAuditDate?.let { date -> - MarketsDateTimeFormatters.formatAsDate(date.millis) - }, - score = provider.securityScore, - urlData = provider.urlData?.let { urlData -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = urlData.fullUrl, - rootHost = urlData.rootHost, - ) - }, - iconUrl = provider.iconUrl, - ) - }, - onProviderLinkClick = onSecurityScoreProviderLinkClick, - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt deleted file mode 100644 index e7aa05eeb5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -@Suppress("LongParameterList") -internal class TokenMarketInfoConverter( - private val appCurrency: Provider, - private val needApplyFCARestrictions: Provider, - private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit, - private val onListedOnClick: (Int) -> Unit, - onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - onLinkClick: (LinksUM.Link) -> Unit, - onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, - onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, - onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - private val insightsConverter = InsightsConverter( - appCurrency = appCurrency, - onInfoClick = onInfoClick, - onIntervalChanged = onInsightsIntervalChanged, - ) - - private val securityScoreConverter = SecurityScoreConverter( - onSecurityScoreInfoClick = onSecurityScoreInfoClick, - onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick, - ) - private val pricePerformanceConverter = PricePerformanceConverter( - appCurrency = appCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - private val linksConverter = LinksConverter(onLinkClick = onLinkClick) - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { - val metricsConverter = MetricsConverter( - tokenSymbol = value.symbol, - appCurrency = appCurrency, - onInfoClick = onInfoClick, - ) - - val exchangesAmount = value.exchangesAmount - val insights = if (needApplyFCARestrictions()) { - null - } else { - value.insights?.let { insightsConverter.convert(it) } - } - val securityScore = if (needApplyFCARestrictions()) { - null - } else { - value.securityData?.let { securityScoreConverter.convert(it) } - } - return MarketsTokenDetailsUM.InformationBlocks( - insights = insights, - securityScore = securityScore, - metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { performance -> - pricePerformanceConverter.convert( - value = performance, - currentPrice = value.quotes.currentPrice, - ) - }, - listedOn = if (exchangesAmount != null && exchangesAmount > 0) { - ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount) - } else { - ListedOnUM.Empty - }, - links = value.links?.let { linksConverter.convert(it) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt deleted file mode 100644 index bc1aeb77e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenQuotes -import java.math.BigDecimal -import java.math.RoundingMode - -internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String { - val percent = when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } - - return percent?.format { percent() }.orEmpty() -} - -internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { - return when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } -} - -@Suppress("MagicNumber") -internal fun BigDecimal?.percentChangeType(): PriceChangeType { - val scaled = this?.setScale(4, RoundingMode.HALF_UP) - return when { - scaled == null -> PriceChangeType.NEUTRAL - scaled > BigDecimal.ZERO -> PriceChangeType.UP - scaled < BigDecimal.ZERO -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -@Suppress("MagicNumber") -internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal { - return if (previousPrice == BigDecimal.ZERO) { - BigDecimal.ZERO - } else { - currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP) - } -} - -internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = getFiatPriceAmountWithScale(value = currentPrice).first - val updated = getFiatPriceAmountWithScale(value = updatedPrice).first - - return when { - updated > current -> PriceChangeType.UP - updated < current -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -internal fun PriceChangeType.toChartType(): MarketChartLook.Type { - return when (this) { - PriceChangeType.UP -> MarketChartLook.Type.Growing - PriceChangeType.DOWN -> MarketChartLook.Type.Falling - PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt deleted file mode 100644 index f36474bbee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.DateTimeFormatters -import com.tangem.core.ui.utils.formatAsDateTime -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.impl.R -import com.tangem.utils.H24_MILLIS -import com.tangem.utils.WEEK_MILLIS -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import java.math.BigDecimal - -internal object MarketsDateTimeFormatters { - - private val dateTimeMMMFormatter by lazy { - DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm") - } - - private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - - fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { - return when (interval) { - PriceChangeInterval.H24 -> { value: BigDecimal -> - value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) - } - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.YEAR -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.ALL_TIME -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY) - } - } - } - - fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { - return when (interval) { - PriceChangeInterval.H24 -> resourceReference(R.string.common_today) - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all) - } - } - - fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { - return when (interval) { - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - } - } - - @Suppress("MagicNumber") - fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long { - return when (interval) { - PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS - PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS - PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis - PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis - PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis - PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis - PriceChangeInterval.ALL_TIME -> 0 - } - } - - fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { - return formatDateByInterval( - interval = interval, - startTimestamp = getStartTimestampByInterval( - interval = interval, - currentTimestamp = currentTimestamp, - ), - ) - } - - fun formatAsDate(timestamp: Long): String { - return timestamp.formatAsDateTime(dateFormatter) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt deleted file mode 100644 index 116fb390b4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenQuotes -import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import org.joda.time.DateTime -import java.math.BigDecimal - -internal class QuotesStateUpdater( - private val currentAppCurrency: Provider, - private val state: MutableStateFlow, - private val currentQuotes: MutableStateFlow, - private val lastUpdatedTimestamp: MutableStateFlow, - private val currentTokenInfo: MutableStateFlow, - private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, -) { - private val pricePerformanceConverter = PricePerformanceConverter( - currentAppCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - - suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, - ) - } else { - consumedEvent() - } - - val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.isMarkerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp.value = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency().symbol, - fiatCurrencyCode = currentAppCurrency().code, - ).price() - }, - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( - stateToUpdate.selectedInterval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), - ) - } - } - - private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { - val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this - - return if (this is MarketsTokenDetailsUM.Body.Content) { - copy( - infoBlocks = infoBlocks.copy( - pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), - ), - ) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt deleted file mode 100644 index dbe01ddcdc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.domain.markets.TokenMarketInfo - -internal sealed class TokenNetworksState { - - data object Loading : TokenNetworksState() - - data object NoNetworksAvailable : TokenNetworksState() - - data class NetworksAvailable(val networks: List) : TokenNetworksState() -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt deleted file mode 100644 index a732ce35a6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.details.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.marketprice.PriceChangeInPercent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.components.* -import com.tangem.features.markets.details.impl.ui.preview.MarketsTokenDetailsPreview -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Suppress("LongParameterList") -@Composable -internal fun MarketsTokenDetailsContent( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarPadding: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - Content( - modifier = modifier, - backgroundColor = backgroundColor, - state = state, - onBackClick = onBackClick, - onHeaderSizeChange = onHeaderSizeChange, - backButtonEnabled = backButtonEnabled, - portfolioBlock = portfolioBlock, - isAccountEnabled = isAccountEnabled, - addTopBarStatusBarInsets = addTopBarStatusBarPadding, - ) - - when (state.bottomSheetConfig.content) { - is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig) - is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig) - is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig) - } -} - -@Suppress("LongParameterList") -@Composable -private fun Content( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarInsets: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - val density = LocalDensity.current - val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } - val lazyListState = rememberLazyListState() - - Column( - modifier = modifier - .drawBehind { drawRect(backgroundColor) } - .let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it } - .fillMaxSize(), - ) { - TopBar( - modifier = Modifier.onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - }, - lazyListState = lazyListState, - tokenName = state.tokenName, - tokenPrice = state.priceText, - isBackButtonEnabled = backButtonEnabled, - onBackClick = onBackClick, - ) - - SpacerH4() - - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item("header") { - Header( - state = state, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH16() } - item("intervalSelector") { - IntervalSelector( - trendInterval = state.selectedInterval, - onIntervalClick = state.onSelectedIntervalChange, - isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH32() } - item("chart") { - MarketTokenDetailsChart( - modifier = Modifier.fillMaxWidth(), - backgroundColor = backgroundColor, - state = state.chartState, - ) - } - item { SpacerH16() } - - tokenMarketDetailsBody( - state = state.body, - isAccountEnabled = isAccountEnabled, - portfolioBlock = portfolioBlock, - ) - } - } -} - -@Composable -private fun TopBar( - lazyListState: LazyListState, - tokenName: String, - tokenPrice: String, - isBackButtonEnabled: Boolean, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val shouldShowPriceSubtitle by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex > 1 - } - } - - TangemTopAppBar( - modifier = modifier, - title = tokenName, - subtitle = if (shouldShowPriceSubtitle) tokenPrice else null, - startButton = TopAppBarButtonUM.Back( - onBackClicked = onBackClick, - enabled = isBackButtonEnabled, - ), - ) -} - -@Composable -private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - TokenPriceText( - price = state.priceText, - triggerPriceChange = state.triggerPriceChange, - ) - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { - Text( - text = state.dateTimeText.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - if (state.priceChangePercentText != null) { - PriceChangeInPercent( - valueInPercent = state.priceChangePercentText, - type = state.priceChangeType, - textStyle = TangemTheme.typography.caption2, - ) - } - } - } - SpacerW4() - CoinIcon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size48), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - } -} - -@Composable -private fun TokenPriceText( - price: String, - triggerPriceChange: StateEvent, - modifier: Modifier = Modifier, -) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember(generalColor) { Animatable(generalColor) } - - EventEffect(triggerPriceChange) { changeType -> - val nextColor = when (changeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@EventEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - - Text( - text = price, - modifier = modifier, - color = color.value, - autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize), - maxLines = 1, - style = TangemTheme.typography.head, - ) -} - -@Composable -private fun IntervalSelector( - trendInterval: PriceChangeInterval, - isEnabled: Boolean, - onIntervalClick: (PriceChangeInterval) -> Unit, - modifier: Modifier = Modifier, -) { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - ), - color = TangemTheme.colors.button.secondary, - initialSelectedItem = trendInterval, - onClick = onIntervalClick, - isEnabled = isEnabled, - modifier = modifier, - ) { - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = it.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = if (isEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.disabled - }, - ) - } - } -} - -@Composable -fun PriceChangeInterval.getText(): TextReference { - return when (this) { - PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title) - PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title) - PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title) - PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title) - PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title) - PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title) - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title) - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun MarketsTokenDetailsContent_Preview( - @PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM, -) { - TangemThemePreview { - MarketsTokenDetailsContent( - state = params, - onHeaderSizeChange = {}, - onBackClick = {}, - backgroundColor = TangemTheme.colors.background.tertiary, - portfolioBlock = {}, - backButtonEnabled = true, - isAccountEnabled = true, - addTopBarStatusBarPadding = false, - ) - } -} - -private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - MarketsTokenDetailsPreview.loadingState, - MarketsTokenDetailsPreview.contentState, - ) -} -// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt deleted file mode 100644 index fdbf284dbc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -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.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.TokenItem -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.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet - * - * @param config bottom sheet config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) }, - content = { content -> - Box { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item(key = "subtitle") { - Subtitle( - subtitleRes = content.subtitleResId, - volumeReference = content.volumeReference, - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing8, - ), - ) - } - - items( - items = content.exchangeItems, - key = TokenItemState::id, - itemContent = { TokenItem(state = it, isBalanceHidden = false) }, - ) - } - - if (content is ExchangesBottomSheetContent.Error) { - Error( - content = content, - modifier = Modifier.align(Alignment.Center), - ) - } - } - }, - ) -} - -@Composable -private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) { - TangemTopAppBar( - title = stringResourceSafe(id = textResId), - startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick), - ) -} - -@Composable -private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SubtitleText(textReference = resourceReference(id = subtitleRes)) - - SubtitleText(textReference = volumeReference) - } -} - -@Composable -private fun SubtitleText(textReference: TextReference) { - Text( - text = textReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResourceSafe(id = content.message), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - ) - - SpacerH12() - - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(id = R.string.alert_button_try_again), - onClick = content.onRetryClick, - ), - ) - } -} - -@Preview -@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ExchangesBottomSheet( - @PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent, -) { - TangemThemePreview { - ExchangesBottomSheet( - config = TangemBottomSheetConfig( - onDismissRequest = {}, - content = content, - isShown = true, - ), - ) - } -} - -private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider( - listOf( - ExchangesBottomSheetContent.Loading(exchangesCount = 13), - ExchangesBottomSheetContent.Error(onRetryClick = {}), - ExchangesBottomSheetContent.Content( - exchangeItems = List(size = 13) { index -> - TokenItemState.Content( - id = index.toString(), - iconState = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.ic_facebook_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"), - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = AuditLabelUM( - text = stringReference("Caution"), - type = AuditLabelUM.Type.Warning, - ), - ), - onItemClick = {}, - onItemLongClick = {}, - ) - } - .toImmutableList(), - ), - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt deleted file mode 100644 index 234468b262..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.impl.R -import dev.jeziellago.compose.markdowntext.MarkdownText - -@Composable -internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - MarkdownText( - markdown = content.body.resolveReference(), - disableLinkMovementMethod = true, - linkifyMask = 0, - syntaxHighlightColor = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - if (content.generatedAINotificationUM != null) { - AdditionalInfoNotification( - onClick = content.generatedAINotificationUM.onClick, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) { - Notification( - config = NotificationConfig( - subtitle = TextReference.Res(id = R.string.information_generated_with_ai), - iconResId = R.drawable.ic_magic_28, - onClick = onClick, - shouldShowArrowIcon = false, - ), - modifier = modifier, - subtitleColor = TangemTheme.colors.text.primary1, - containerColor = TangemTheme.colors.button.disabled, - iconTint = TangemTheme.colors.icon.accent, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt deleted file mode 100644 index 3ca286968f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.tangem.features.markets.details.impl.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.graphics.vector.ImageVector -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.R -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM - -@Composable -internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (infoPointUM.onInfoClick != null) { - TooltipText( - text = infoPointUM.title, - onInfoClick = infoPointUM.onInfoClick, - textStyle = TangemTheme.typography.caption2, - ) - } else { - Text( - text = infoPointUM.title.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Row { - Text( - text = infoPointUM.value, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - if (infoPointUM.change != null) { - SpacerW4() - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size8) - .align(Alignment.CenterVertically), - imageVector = ImageVector.vectorResource( - id = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8 - InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8 - }, - ), - tint = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent - InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning - }, - contentDescription = null, - ) - } - } - } -} - -@Composable -internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (withTooltip) { - Box( - modifier = Modifier - .requiredHeight(TangemTheme.dimens.size16) - .fillMaxWidth(), - contentAlignment = Alignment.CenterStart, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = false, - ) - } - } else { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body1, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.UP, - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = { }, - ), - ) - } - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewShimmer() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPointShimmer(modifier = Modifier.fillMaxWidth()) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - } - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt deleted file mode 100644 index d40e37b970..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -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.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -@Composable -internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - TooltipText( - text = resourceReference(R.string.markets_token_details_insights), - textStyle = TangemTheme.typography.subtitle2, - onInfoClick = state.onInfoClick, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = 4.dp, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val infoPoints = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24Info - PriceChangeInterval.WEEK -> state.weekInfo - PriceChangeInterval.MONTH -> state.monthInfo - else -> state.h24Info - } - - GridItems( - items = infoPoints, - itemContent = { infoPoint -> - InfoPoint( - modifier = Modifier.align(Alignment.CenterStart), - infoPointUM = infoPoint, - ) - }, - ) - }, - ) -} - -@Composable -internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - GridItems( - items = List(size = 4) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - ) - }, - ) - }, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - InsightsBlock( - state = InsightsUM( - h24Info = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000 000", - ), - ), - weekInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000", - ), - ), - monthInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000", - ), - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { ContentPreview() }, - shimmerContent = { InsightsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt deleted file mode 100644 index 6d10ec8957..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.ChipShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.chip.Chip -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -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.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_links), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - content = { - Column { - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_official_links), - links = state.officialLinks, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_social), - links = state.social, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_repository), - links = state.repository, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site), - links = state.blockchainSite, - onLinkClick = state.onLinkClick, - lastBlock = true, - ) - } - }, - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun SubBlock( - links: ImmutableList, - onLinkClick: (LinksUM.Link) -> Unit, - modifier: Modifier = Modifier, - lastBlock: Boolean = false, - title: String = "Official links", -) { - if (links.isEmpty()) return - - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = title, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - links.fastForEach { link -> - Chip( - text = stringReference(link.title), - iconResId = link.iconRes, - onClick = { onLinkClick(link) }, - ) - } - } - } - } -} - -@Composable -fun LinksBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - ) - }, - content = { - Column { - SubBlockPlaceholder() - SubBlockPlaceholder() - SubBlockPlaceholder(lastBlock = true) - } - }, - ) -} - -@Composable -private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) { - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - TextShimmer( - modifier = Modifier.width(78.dp), - style = TangemTheme.typography.caption2, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - repeat(times = 3) { - ChipShimmer( - modifier = Modifier.weight(1f), - ) - } - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - LinksBlock( - state = LinksUM( - officialLinks = persistentListOf( - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - social = persistentListOf( - LinksUM.Link( - title = "Twitter", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Facebook", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - repository = persistentListOf( - LinksUM.Link( - title = "Github", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - blockchainSite = persistentListOf(), - onLinkClick = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { LinksBlockPlaceholder() }, - actualContent = { ContentPreview() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt deleted file mode 100644 index 3d5714135e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -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.common.ui.R -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import kotlinx.coroutines.delay - -/** - * "Listed on" block - * - * @param state block state - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) { - Box(modifier = modifier) { - InformationBlock( - title = { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - modifier = Modifier - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .clickable(enabled = state is ListedOnUM.Content) { - (state as? ListedOnUM.Content)?.onClick?.invoke() - }, - ) { - Description( - state = state, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - - if (state is ListedOnUM.Content) { - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - ) - } - } -} - -@Composable -internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - title = { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - ) - }, - modifier = modifier, - ) { - TextShimmer( - style = TangemTheme.typography.body2, - modifier = Modifier - .fillMaxWidth(fraction = 0.3f) - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } -} - -@Composable -private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) { - Text( - text = state.description.resolveReference(), - modifier = modifier, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Preview(widthDp = 328, heightDp = 68) -@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) { - TangemThemePreview { - if (state == null) { - ListedOnBlockPlaceholder() - } else { - ListedOnBlock(state = state) - } - } -} - -@Preview -@Composable -private fun Preview_ListedOnBlock_StateChanging() { - var state by remember { mutableStateOf(value = null) } - - Preview_ListedOnBlock(state = state) - - LaunchedEffect(key1 = null) { - delay(timeMillis = 3000) - - state = ListedOnUM.Empty - } -} - -private class ListenOnUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - ListedOnUM.Empty, - ListedOnUM.Content(onClick = {}, amount = 5), - null, - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt deleted file mode 100644 index 0ad6b22ee0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.CircularProgressIndicator -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.graphics.Color -import com.tangem.common.ui.charts.MarketChart -import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.common.ui.charts.state.rememberMarketChartState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.core.ui.components.UnableToLoadData - -@Composable -internal fun MarketTokenDetailsChart( - state: MarketsTokenDetailsUM.ChartState, - backgroundColor: Color, - modifier: Modifier = Modifier, -) { - val growingColor = TangemTheme.colors.icon.accent - val fallingColor = TangemTheme.colors.icon.warning - val neutralColor = TangemTheme.colors.icon.informative - - val chartState = rememberMarketChartState( - dataProducer = state.dataProducer, - colorMapper = { chartType -> - when (chartType) { - MarketChartLook.Type.Growing -> growingColor - MarketChartLook.Type.Falling -> fallingColor - MarketChartLook.Type.Neutral -> neutralColor - } - }, - onMarkerShown = state.onMarkerPointSelected, - ) - - val bottomChartAxisHeight = getMarketChartBottomAxisHeight() - - Box(modifier) { - MarketChart( - modifier = Modifier.fillMaxWidth(), - state = chartState, - ) - - if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) { - Box( - Modifier - .drawBehind { drawRect(backgroundColor) } - .matchParentSize() - .padding(bottom = bottomChartAxisHeight), - ) { - when (state.status) { - MarketsTokenDetailsUM.ChartState.Status.LOADING -> { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .align(Alignment.Center), - color = TangemTheme.colors.text.accent, - strokeWidth = TangemTheme.dimens.size2, - ) - } - MarketsTokenDetailsUM.ChartState.Status.ERROR -> { - UnableToLoadData( - modifier = Modifier - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ) - .align(Alignment.Center), - onRetryClick = state.onLoadRetryClick, - ) - } - else -> {} - } - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt deleted file mode 100644 index 98e74fd06f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -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.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -const val MAX_METRICS_COUNT = 6 - -@Composable -internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { - var isExpanded by remember { mutableStateOf(false) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_metrics), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - if (state.metrics.size > MAX_METRICS_COUNT) { - ShowLessMoreButton(expanded = isExpanded, onClick = { isExpanded = !isExpanded }) - } - }, - content = { - val metrics = if (isExpanded) { - state.metrics - } else { - state.metrics.take(MAX_METRICS_COUNT).toImmutableList() - } - - GridItems( - items = metrics, - itemContent = { - InfoPoint(infoPointUM = it) - }, - ) - }, - ) -} - -// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock -@Composable -private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) { - // FIXME add string resources - val text = if (expanded) { - "See less" - } else { - "See more" - } - - TextButton( - text = text, - onClick = onClick, - colors = TangemButtonsDefaults.positiveButtonColors, - textStyle = TangemTheme.typography.body2, - ) -} - -@Composable -internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - style = TangemTheme.typography.subtitle2, - ) - }, - action = { - Box(Modifier) - }, - content = { - GridItems( - items = List(size = 6) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - }, - ) - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BlockPreview() { - TangemThemePreview { - MetricsBlock( - state = MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = "A", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_total_supply), - value = "1.2T", - onInfoClick = {}, - ), - ), - ), - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { BlockPreview() }, - shimmerContent = { MetricsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt deleted file mode 100644 index 17e8d19e3f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.platform.LocalDensity -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.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemAnimations -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_price_performance), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.MONTH, - PriceChangeInterval.ALL_TIME, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val value = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24 - PriceChangeInterval.MONTH -> state.month - PriceChangeInterval.ALL_TIME -> state.all - else -> error("") - } - - Content( - modifier = Modifier.fillMaxWidth(), - state = value, - ) - }, - ) -} - -@Composable -private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) { - val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState( - targetFraction = state.indicatorFraction, - ) - - Column( - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResourceSafe(R.string.markets_token_details_low), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - SpacerW8() - Text( - text = stringResourceSafe(R.string.markets_token_details_high), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - TangemLinearProgressIndicator( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - progress = { animatedIndicatorFraction }, - color = TangemTheme.colors.text.accent, - backgroundColor = TangemTheme.colors.background.tertiary, - strokeCap = StrokeCap.Round, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - modifier = Modifier.weight(1f), - text = state.low, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.weight(1f), - text = state.high, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ) - } - } -} - -@Composable -internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - Column( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - } - RectangleShimmer( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - radius = 27.dp, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - } - } - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - PricePerformanceBlock( - modifier = Modifier, - state = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "\$38,5K", - high = "\$58,5K", - indicatorFraction = 0.5f, - ), - month = PricePerformanceUM.Value( - low = "\$500,5K", - high = "\$5800,5K", - indicatorFraction = 0.8f, - ), - all = PricePerformanceUM.Value( - low = "\$58,52", - high = "\$580,5M", - indicatorFraction = 0.2f, - ), - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - PricePerformanceBlockPlaceholder() - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt deleted file mode 100644 index d7d047c38b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.annotation.FloatRange -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.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.impl.R -import kotlin.math.round - -private const val STARS_COUNT = 5 - -@Composable -internal fun ScoreStarsBlock( - score: Float, - horizontalSpacing: Dp, - scoreTextStyle: TextStyle, - modifier: Modifier = Modifier, -) { - val rounded = score.roundTo1decimal() - val percentage = rounded / STARS_COUNT - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = rounded.toString(), - style = scoreTextStyle, - color = TangemTheme.colors.text.primary1, - ) - Stars(fraction = percentage) - } -} - -@Suppress("MagicNumber") -@Composable -private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { - val grayColor = TangemTheme.colors.icon.inactive - - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - repeat(times = 5) { i -> - Box( - modifier = Modifier.size(TangemTheme.dimens.size16), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(16.dp) - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .drawWithCache { - onDrawWithContent { - val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) - val starFractionFloat = starFraction - .toFloat() - .roundTo1decimal() - - drawContent() - drawRect( - color = grayColor, - topLeft = Offset(x = size.width * starFractionFloat, y = 0f), - size = Size(size.width * (1 - starFractionFloat), size.height), - blendMode = BlendMode.SrcIn, - ) - } - }, - imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } - } -} - -@Suppress("MagicNumber") -private fun Float.roundTo1decimal(): Float { - return round(this * 10) / 10 -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt deleted file mode 100644 index 468b7f7b59..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -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.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R - -@Composable -internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier - .weight(1F) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TooltipText( - text = resourceReference(R.string.markets_token_details_security_score), - onInfoClick = state.onInfoClick, - textStyle = TangemTheme.typography.subtitle2, - ) - - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - ScoreStarsBlock( - score = state.score, - scoreTextStyle = TangemTheme.typography.body1, - horizontalSpacing = TangemTheme.dimens.spacing8, - ) - } -} - -@Composable -internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier - .fillMaxWidth(fraction = 0.4f) - .padding(vertical = TangemTheme.dimens.spacing2) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - textSizeHeight = true, - ) - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, locale = "ru") -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - SecurityScoreBlock( - state = SecurityScoreUM( - score = 3.5f, - description = stringReference("Based on 3 ratings"), - onInfoClick = {}, - ), - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - SecurityScoreBlockPlaceholder( - modifier = Modifier.fillMaxWidth(), - ) - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt deleted file mode 100644 index b8c4242c9a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -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.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.util.fastForEachIndexed -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -@Composable -internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Text( - text = content.description.resolveReference(), - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - SpacerH12() - content.providers.fastForEachIndexed { index, provider -> - DividerContainer( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.providers.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - showDivider = index != content.providers.lastIndex, - ) { - SecurityScoreProviderRow( - providerUM = provider, - onLinkClick = { content.onProviderLinkClick(provider) }, - ) - } - } - - SpacerH16() - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun SecurityScoreProviderRow( - providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM, - onLinkClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing12) - .heightIn(min = TangemTheme.dimens.size68), - verticalAlignment = Alignment.CenterVertically, - ) { - SubcomposeAsyncImage( - modifier = Modifier - .size(size = TangemTheme.dimens.size40) - .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(context = LocalContext.current) - .data(providerUM.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), - loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - contentDescription = null, - ) - - Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = providerUM.name, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - if (providerUM.lastAuditDate != null) { - Text( - text = providerUM.lastAuditDate, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - - SpacerWMax() - - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - modifier = Modifier.clickable( - enabled = providerUM.urlData != null, - indication = ripple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - onClick = onLinkClick, - ), - ) { - ScoreStarsBlock( - score = providerUM.score, - scoreTextStyle = TangemTheme.typography.body2, - horizontalSpacing = TangemTheme.dimens.spacing3, - ) - - UrlBlock(providerUM) - } - } -} - -@Composable -private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) { - val urlData = providerUM.urlData - val rootHost = urlData?.rootHost - if (urlData != null && rootHost != null) { - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = urlData.rootHost, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_arrow_top_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - SecurityScoreBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = SecurityScorePreviewData.bottomSheetContent, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt deleted file mode 100644 index 13124c707d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -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.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.items.DescriptionItem -import com.tangem.core.ui.components.items.DescriptionPlaceholder -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R - -@Suppress("CanBeNonNullable") -internal fun LazyListScope.tokenMarketDetailsBody( - state: MarketsTokenDetailsUM.Body, - isAccountEnabled: Boolean, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - when (state) { - MarketsTokenDetailsUM.Body.Loading -> { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - loadingInfoBlocks() - } - is MarketsTokenDetailsUM.Body.Content -> { - if (state.description != null) { - description(state.description) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - infoBlocksList(state.infoBlocks) - } - is MarketsTokenDetailsUM.Body.Error -> { - error(state) - } - MarketsTokenDetailsUM.Body.Nothing -> { - // Do nothing - } - } -} - -private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) { - item("body-error") { - Box(Modifier.fillMaxWidth()) { - UnableToLoadData( - modifier = Modifier - .align(Alignment.Center) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing40, - ), - onRetryClick = state.onLoadRetryClick, - ) - } - } -} - -private fun LazyListScope.aboutCoinHeader() { - item("aboutCoinHeader") { - Text( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing20, - ), - text = stringResourceSafe(R.string.markets_about_coin_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h3, - ) - } -} - -private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) { - item("description") { - DescriptionItem( - modifier = Modifier.blockPaddings(), - description = description.shortDescription, - hasFullDescription = description.fullDescription != null, - onReadMoreClick = description.onReadMoreClick, - ) - } -} - -internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) { - if (state.insights != null) { - item("insights") { - InsightsBlock( - modifier = Modifier.blockPaddings(), - state = state.insights, - ) - } - } - - if (state.securityScore != null) { - item("securityScore") { - SecurityScoreBlock( - modifier = Modifier.blockPaddings(), - state = state.securityScore, - ) - } - } - - if (state.metrics != null) { - item("metrics") { - MetricsBlock( - modifier = Modifier.blockPaddings(), - state = state.metrics, - ) - } - } - - if (state.pricePerformance != null) { - item("pricePerformance") { - PricePerformanceBlock( - modifier = Modifier.blockPaddings(), - state = state.pricePerformance, - ) - } - } - - item(key = "listedOn") { - ListedOnBlock( - state = state.listedOn, - modifier = Modifier.blockPaddings(), - ) - } - - if (state.links != null) { - item("links") { - LinksBlock( - modifier = Modifier.blockPaddings(), - state = state.links, - ) - } - } -} - -private fun LazyListScope.loadingInfoBlocks() { - item("insights-loading") { - InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("securityScore-loading") { - SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("metrics-loading") { - MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("pricePerformance-loading") { - PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item(key = "listedOn-loading") { - ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("links-loading") { - LinksBlockPlaceholder(modifier = Modifier.blockPaddings()) - } -} - -@Composable -private fun Modifier.blockPaddings(): Modifier { - return this.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt deleted file mode 100644 index 83be0ff031..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.* -import kotlinx.collections.immutable.persistentListOf - -internal object MarketsTokenDetailsPreview { - private val infoPoint = InfoPointUM( - title = stringReference("1"), - value = "2", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = {}, - ) - - val loadingState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Loading, - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = false, - triggerPriceChange = consumedEvent(), - ) - - val contentState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Content( - description = MarketsTokenDetailsUM.Description( - shortDescription = stringReference("markets_token_details_description_short"), - fullDescription = stringReference("markets_token_details_description_full"), - onReadMoreClick = {}, - ), - infoBlocks = MarketsTokenDetailsUM.InformationBlocks( - insights = InsightsUM( - h24Info = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - weekInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - monthInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - securityScore = SecurityScoreUM( - score = 2.3f, - description = stringReference("markets_token_details_security_score_description"), - onInfoClick = {}, - ), - metrics = MetricsUM( - metrics = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - ), - pricePerformance = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - month = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - all = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - onIntervalChanged = {}, - ), - listedOn = ListedOnUM.Empty, - links = null, - ), - ), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = true, - triggerPriceChange = consumedEvent(), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt deleted file mode 100644 index d80e49af6c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -internal object SecurityScorePreviewData { - - val bottomSheetContent = SecurityScoreBottomSheetContent( - title = stringReference("Security score"), - description = stringReference( - "Security score of a token is a metric that assesses the " + - "security level of a blockchain or token based on various factors and is compiled from " + - "the sources listed below.", - ), - providers = listOf( - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Moralis", - lastAuditDate = "21.10.2024", - score = 4.9F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://moralis.com/", - rootHost = "moralis.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Certik", - lastAuditDate = "10.07.2024", - score = 4.6F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://certik.com/", - rootHost = "certik.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Cyberscope", - lastAuditDate = "25.06.2023", - score = 4.5F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://cyberscope.com/", - rootHost = "cyberscope.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "TokenInsight", - lastAuditDate = "17.01.2022", - score = 4.0F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://tokeninsight.com/", - rootHost = "tokeninsight.com", - ), - iconUrl = "", - ), - - ), - onProviderLinkClick = {}, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt deleted file mode 100644 index cccbf85d31..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.StringRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.plus -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.utils.StringsSigns.DOT -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet content - * -[REDACTED_AUTHOR] - */ -internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent { - - /** Title of bottom sheet. Like, app bar. */ - @get:StringRes - val titleResId: Int - get() = R.string.markets_token_details_exchanges_title - - /** Subtitle */ - @get:StringRes - val subtitleResId: Int - get() = R.string.markets_token_details_exchange - - /** Volume info */ - @get:StringRes - val volumeReference: TextReference - get() = resourceReference(id = R.string.markets_token_details_volume) + - stringReference(value = " $DOT ") + - resourceReference(id = R.string.markets_selector_interval_24h_title) - - /** Exchange items */ - val exchangeItems: ImmutableList - - /** - * Loading state - * - * @property exchangesCount count of exchanges - */ - data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent { - - override val exchangeItems: ImmutableList - get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") } - .toImmutableList() - } - - /** - * Content state - * - * @property exchangeItems exchanges - */ - data class Content( - override val exchangeItems: ImmutableList, - ) : ExchangesBottomSheetContent - - /** Error state */ - data class Error( - val onRetryClick: () -> Unit, - ) : ExchangesBottomSheetContent { - override val exchangeItems: ImmutableList = persistentListOf() - - @StringRes - val message: Int = R.string.markets_loading_error_title - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt deleted file mode 100644 index d6af118609..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoBottomSheetContent( - val title: TextReference, - val body: TextReference, - val generatedAINotificationUM: GeneratedAINotificationUM? = null, -) : TangemBottomSheetConfigContent { - - data class GeneratedAINotificationUM(val onClick: () -> Unit) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt deleted file mode 100644 index 383db4e627..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoPointUM( - val title: TextReference, - val value: String, - val change: ChangeType? = null, - val onInfoClick: (() -> Unit)? = null, -) { - enum class ChangeType { - UP, DOWN - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt deleted file mode 100644 index e2d0b3270b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.domain.markets.PriceChangeInterval -import kotlinx.collections.immutable.ImmutableList - -internal data class InsightsUM( - val h24Info: ImmutableList, - val weekInfo: ImmutableList, - val monthInfo: ImmutableList, - val onInfoClick: () -> Unit, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt deleted file mode 100644 index 5b4ea87e18..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.DrawableRes -import kotlinx.collections.immutable.ImmutableList - -internal data class LinksUM( - val officialLinks: ImmutableList, - val social: ImmutableList, - val repository: ImmutableList, - val blockchainSite: ImmutableList, - val onLinkClick: (Link) -> Unit, -) { - data class Link( - @DrawableRes val iconRes: Int, - val title: String, - val url: String, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt deleted file mode 100644 index a853675b10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.markets.impl.R - -/** - * "Listed on" block UI model - * -[REDACTED_AUTHOR] - */ -internal sealed interface ListedOnUM { - - /** Title */ - val title: TextReference - get() = resourceReference(id = R.string.markets_token_details_listed_on) - - /** Description */ - val description: TextReference - - /** Empty state. No exchanges found */ - data object Empty : ListedOnUM { - override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges) - } - - /** - * Content with number of exchanges - * - * @property onClick lambda be invoked when button is clicked - * @property amount amount of exchanges - */ - data class Content( - val onClick: () -> Unit, - private val amount: Int, - ) : ListedOnUM { - override val description: TextReference = pluralReference( - id = R.plurals.markets_token_details_amount_exchanges, - count = amount, - formatArgs = wrappedList(amount), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt deleted file mode 100644 index 94f581efd7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.markets.PriceChangeInterval -import java.math.BigDecimal - -internal data class MarketsTokenDetailsUM( - val tokenName: String, - val priceText: String, - val iconUrl: String?, - val dateTimeText: TextReference, - val priceChangePercentText: String?, - val priceChangeType: PriceChangeType, - val selectedInterval: PriceChangeInterval, - val isMarkerSet: Boolean, - val chartState: ChartState, - val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, - val bottomSheetConfig: TangemBottomSheetConfig, - val triggerPriceChange: StateEvent, - val body: Body, -) { - - data class ChartState( - val status: Status, - val dataProducer: MarketChartDataProducer, - val onLoadRetryClick: () -> Unit, - val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, - ) { - enum class Status { - LOADING, ERROR, DATA - } - } - - data class InformationBlocks( - val insights: InsightsUM?, - val securityScore: SecurityScoreUM?, - val metrics: MetricsUM?, - val pricePerformance: PricePerformanceUM?, - val listedOn: ListedOnUM, - val links: LinksUM?, - ) - - @Immutable - sealed interface Body { - - data class Error( - val onLoadRetryClick: () -> Unit, - ) : Body - - data object Loading : Body - - data class Content( - val description: Description?, - val infoBlocks: InformationBlocks, - ) : Body - - data object Nothing : Body - } - - data class Description( - val shortDescription: TextReference, - val fullDescription: TextReference?, - val onReadMoreClick: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt deleted file mode 100644 index 8b28533fb3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import kotlinx.collections.immutable.ImmutableList - -internal data class MetricsUM( - val metrics: ImmutableList, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt deleted file mode 100644 index 9448472a0d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.domain.markets.PriceChangeInterval - -internal data class PricePerformanceUM( - val h24: Value, - val month: Value, - val all: Value, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - data class Value( - val low: String, - val high: String, - @FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt deleted file mode 100644 index ceb100bdfb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreBottomSheetContent( - val title: TextReference, - val description: TextReference, - val providers: List, - val onProviderLinkClick: (SecurityScoreProviderUM) -> Unit, -) : TangemBottomSheetConfigContent { - - data class SecurityScoreProviderUM( - val name: String, - val lastAuditDate: String?, - val score: Float, - val urlData: UrlData?, - val iconUrl: String?, - ) { - data class UrlData( - val fullUrl: String, - val rootHost: String?, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt deleted file mode 100644 index d4ebd3a9ee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreUM( - @FloatRange(from = 0.0, to = 5.0) val score: Float, - val description: TextReference, - val onInfoClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt deleted file mode 100644 index 9f441298d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.popWhile -import com.arkivanov.decompose.value.Value -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child -import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsEntryComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - private val marketsEntryChildFactory: MarketsEntryChildFactory, -) : MarketsEntryComponent, AppComponentContext by context { - - private val stackNavigation = StackNavigation() - - private val innerRouter = InnerRouter( - stackNavigation = stackNavigation, - popCallback = { onChildBack() }, - ) - - private val stack: Value> = childStack( - key = "main", - source = stackNavigation, - serializer = Child.serializer(), - initialConfiguration = Child.TokenList, - handleBackButton = false, - childFactory = { configuration, factoryContext -> - marketsEntryChildFactory.createChild( - child = configuration, - appComponentContext = childByContext( - componentContext = factoryContext, - router = innerRouter, - ), - onTokenClick = ::marketsListTokenSelected, - ) - }, - ) - - @Suppress("LongMethod") - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - EntryBottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - stackState = stack.subscribeAsState(), - modifier = modifier, - ) - } - - @OptIn(ExperimentalDecomposeApi::class) - private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { - innerRouter.push( - route = Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params( - token = token, - appCurrency = appCurrency, - shouldShowPortfolio = true, - analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), - ) - } - - private fun onChildBack() { - if (stack.value.active.configuration !is Child.TokenList) { - stackNavigation.popWhile { it != Child.TokenList } - } - } - - @AssistedFactory - interface Factory : MarketsEntryComponent.Factory { - override fun create(context: AppComponentContext): DefaultMarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt deleted file mode 100644 index 785ef74cdb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent -import kotlinx.serialization.Serializable -import javax.inject.Inject - -internal class MarketsEntryChildFactory @Inject constructor( - private val tokenListComponentFactory: MarketsTokenListComponent.FactoryBottomSheet, - private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, -) { - - @Serializable - @Immutable - sealed interface Child : Route { - - @Serializable - @Immutable - data object TokenList : Child - - @Serializable - @Immutable - data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child - } - - fun createChild( - child: Child, - appComponentContext: AppComponentContext, - onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { - return when (child) { - is Child.TokenDetails -> { - tokenDetailsComponentFactory.create( - context = appComponentContext, - params = child.params, - ) - } - is Child.TokenList -> { - tokenListComponentFactory.create( - context = appComponentContext, - params = Unit, - onTokenClick = onTokenClick, - ) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt deleted file mode 100644 index 4603041300..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.entry.impl.di - -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.DefaultMarketsEntryComponent -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 bindMarketsEntryComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt deleted file mode 100644 index 7814672735..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.markets.entry.impl.ui - -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector4D -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.ChildStack -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent - -@Composable -internal fun EntryBottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - stackState: State>, - modifier: Modifier = Modifier, -) { - val primary = TangemTheme.colors.background.primary - val backgroundColor = remember { Animatable(primary) } - - LocalMainBottomSheetColor.current.value = backgroundColor.value - - Children( - stack = stackState.value, - animation = stackAnimation(slide()), - modifier = modifier, - ) { child -> - when (child.configuration) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - (child.instance as MarketsTokenDetailsComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - (child.instance as MarketsTokenListComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - } - } - - val activeChild = stackState.value.active.configuration - - BackgroundColorEffects( - activeChild = activeChild, - backgroundColor = backgroundColor, - bottomSheetState = bottomSheetState, - ) -} - -@Composable -private fun BackgroundColorEffects( - activeChild: MarketsEntryChildFactory.Child, - backgroundColor: Animatable, - bottomSheetState: State, -) { - val primary = TangemTheme.colors.background.primary - val tertiary = TangemTheme.colors.background.tertiary - - // Order of LaunchedEffects is important here - - LaunchedEffect(activeChild) { - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 500), - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 500), - ) - } - } - } - - LaunchedEffect(bottomSheetState.value) { - if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { - when (bottomSheetState.value) { - BottomSheetState.EXPANDED -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 100), - ) - } - BottomSheetState.COLLAPSED -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 100), - ) - } - } - } - } - - LaunchedEffect(primary, tertiary) { - if (backgroundColor.isRunning) return@LaunchedEffect - - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(tertiary) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.snapTo(primary) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt deleted file mode 100644 index f35084b04a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent - -internal interface AddToPortfolioComponent : ComposableBottomSheetComponent { - - data class Params( - val addToPortfolioManager: AddToPortfolioManager, - val callback: Callback, - ) - - interface Callback { - fun onDismiss() - } - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt deleted file mode 100644 index 28daa1389c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent.AnalyticsParams -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -internal interface AddToPortfolioManager { - - val token: TokenMarketParams - val analyticsParams: AnalyticsParams? - val portfolioFetcher: PortfolioFetcher - - val state: StateFlow - - val allAvailableNetworks: Flow> - fun setTokenNetworks(networks: List) - - sealed interface State { - data object Init : State - data class AvailableToAdd( - val availableToAddData: AvailableToAddData, - ) : State - - data object NothingToAdd : State - } - - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: AnalyticsParams?, - ): AddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt deleted file mode 100644 index 976ad0a6c4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -internal data class AvailableToAddData( - val availableToAddWallets: Map, -) { - val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd } - val isSinglePortfolio: Boolean - get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 -} - -internal data class AvailableToAddWallet( - val userWallet: UserWallet, - val accounts: List, - val availableNetworks: Set, - val availableToAddAccounts: Map, -) { - val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd } -} - -@Serializable -internal data class AvailableToAddAccount( - val account: AccountStatus, - val availableNetworks: Set, - val addedNetworks: Set, -) { - - val isSingleNetwork: Boolean - get() = availableNetworks.size == 1 - - val availableToAddNetworks: Set = availableNetworks - .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } - .toSet() - - val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() - - val addedMarketNetworks: Set = availableNetworks - .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } - .toSet() -} - -@Serializable -internal data class SelectedPortfolio( - val userWallet: UserWallet, - val account: AvailableToAddAccount, - val isAccountMode: Boolean, - val hasMorePortfoliosAvailable: Boolean, -) - -internal data class SelectedNetwork( - val selectedNetwork: TokenMarketInfo.Network, - val cryptoCurrency: CryptoCurrency, - val hasMoreNetworksAvailable: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt deleted file mode 100644 index ed74502649..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.common.ui.addtoken.AddTokenContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class AddTokenComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: AddTokenModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val um = state.value ?: return - AddTokenContent( - modifier = modifier, - state = um, - ) - } - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val selectedPortfolio: Flow, - val selectedNetwork: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onChangeNetworkClick() - fun onChangePortfolioClick() - fun onTokenAdded(status: CryptoCurrencyStatus) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): AddTokenComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt deleted file mode 100644 index e954b972bd..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.ui.ChooseNetworkContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class ChooseNetworkComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: ChooseNetworkModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - ChooseNetworkContent(state) - } - - data class Params( - val selectedPortfolio: SelectedPortfolio, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onNetworkSelected(network: TokenMarketInfo.Network) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): ChooseNetworkComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt deleted file mode 100644 index 81c2f24a63..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -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.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.backStack -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop -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.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent.Params -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioRoutes -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultAddToPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, - addTokenComponentFactory: AddTokenComponent.Factory, - tokenActionsComponentFactory: TokenActionsComponent.Factory, - private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory, -) : AppComponentContext by context, AddToPortfolioComponent { - - private val model: AddToPortfolioModel = getOrCreateModel(params) - - private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create( - context = child("portfolioSelectorComponent"), - params = PortfolioSelectorComponent.Params( - portfolioFetcher = model.portfolioFetcher, - controller = model.portfolioSelectorController, - ), - ) - - private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create( - context = child("addTokenComponent"), - params = AddTokenComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - selectedPortfolio = model.selectedPortfolio, - selectedNetwork = model.selectedNetwork, - ), - ) - - private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create( - context = child("tokenActionsComponent"), - params = TokenActionsComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - data = model.tokenActionsData, - ), - ) - - private val childStack = childStack( - key = "addToPortfolioStack", - handleBackButton = true, - source = model.navigation, - serializer = AddToPortfolioRoutes.serializer(), - initialStack = { model.currentStack }, - childFactory = ::contentChild, - ) - - private fun onBack() { - if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss() - } - - override fun dismiss() { - params.callback.onDismiss() - } - - @Composable - override fun BottomSheet() { - val stack by childStack.subscribeAsState() - val contentStack = remember { mutableStateOf(stack) } - val currentRoute = stack.active.configuration - val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty - if (isNotEmpty) { - contentStack.value = stack - } - - TangemModalBottomSheet( - scrollableContent = false, - onBack = ::onBack, - config = TangemBottomSheetConfig( - isShown = isNotEmpty, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - containerColor = TangemTheme.colors.background.tertiary, - title = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - BottomSheetTitle( - stack = stack, - onBackClick = ::onBack, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - content = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - val paddingModifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ) - val isScrollableContent = when (stack.active.configuration) { - AddToPortfolioRoutes.PortfolioSelector -> false - AddToPortfolioRoutes.AddToken, - AddToPortfolioRoutes.Empty, - is AddToPortfolioRoutes.NetworkSelector, - AddToPortfolioRoutes.TokenActions, - -> true - } - if (isScrollableContent) { - Column( - modifier = paddingModifier.verticalScroll(rememberScrollState()), - ) { - stack.active.instance.Content(modifier = Modifier) - } - } else { - stack.active.instance.Content(modifier = paddingModifier) - } - } - }, - ) - } - - @Composable - private fun BottomSheetTitle( - stack: ChildStack, - onBackClick: (() -> Unit), - modifier: Modifier = Modifier, - ) { - val title: TextReference = when (stack.active.configuration) { - AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token) - AddToPortfolioRoutes.Empty -> TextReference.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network) - AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token) - AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent) - .title.collectAsStateWithLifecycle().value - } - val startIconRes: Int? - val endIconRes: Int? - if (stack.backStack.isNotEmpty()) { - startIconRes = R.drawable.ic_back_24 - endIconRes = null - } else { - startIconRes = null - endIconRes = R.drawable.ic_close_24 - } - TangemModalBottomSheetTitle( - modifier = modifier, - title = title, - startIconRes = startIconRes, - endIconRes = endIconRes, - onStartClick = onBackClick, - onEndClick = onBackClick, - ) - } - - private fun contentChild( - config: AddToPortfolioRoutes, - componentContext: ComponentContext, - ): ComposableContentComponent = when (config) { - AddToPortfolioRoutes.AddToken -> addTokenComponent - AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent - AddToPortfolioRoutes.TokenActions -> tokenActionsComponent - AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create( - context = childByContext(componentContext), - params = ChooseNetworkComponent.Params( - selectedPortfolio = config.selectedPortfolio, - callbacks = model, - ), - ) - } - - @AssistedFactory - interface Factory : AddToPortfolioComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultAddToPortfolioComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt deleted file mode 100644 index 93bf26a76f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import com.tangem.features.markets.portfolio.add.impl.ui.TokenActionsContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class TokenActionsComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: TokenActionsModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = TokenReceiveConfig.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - val tokenActionsUM = state.value ?: return - TokenActionsContent( - modifier = modifier, - state = tokenActionsUM, - ) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: TokenReceiveConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val data: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onLaterClick() - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): TokenActionsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt deleted file mode 100644 index 3a311b6854..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.converter - -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AvailableToAddAccount -import com.tangem.features.markets.portfolio.add.api.AvailableToAddData -import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet -import javax.inject.Inject - -internal class AvailableToAddDataConverter @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, -) { - - suspend fun convert( - balances: Map, - availableNetworks: Set, - marketParams: TokenMarketParams, - ): AvailableToAddData { - suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { - val currencies = availableNetworks - .mapNotNull { network -> - createCryptoCurrency( - userWallet = wallet, - network = network, - marketParams = marketParams, - account = this.account, - ) - } - - if (currencies.isEmpty()) return null - - val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, currencies) - .fold( - ifEmpty = { emptySet() }, - ifSome = { map -> - map.values.flatMapTo(hashSetOf()) { statuses -> - statuses.map { it.currency.network } - } - }, - ) - - return AvailableToAddAccount( - account = this, - availableNetworks = availableNetworks, - addedNetworks = addedNetworks, - ) - } - - suspend fun getAvailableToAddWallet( - entry: Map.Entry, - ): AvailableToAddWallet { - val (_, balance) = entry - val wallet = balance.userWallet - val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks) - val accounts = balance.accountsBalance.accountStatuses - val availableToAddAccounts: Map = accounts - .mapNotNull { accountStatus -> - val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null - accountStatus.account.accountId to availableToAddAccount - } - .toMap() - return AvailableToAddWallet( - userWallet = wallet, - accounts = accounts, - availableNetworks = filteredNetworks, - availableToAddAccounts = availableToAddAccounts, - ) - } - - val availableToAddWallets: Map = balances - .map { entry -> - val (walletId, _) = entry - val availableToAddWallet = getAvailableToAddWallet(entry) - walletId to availableToAddWallet - } - .filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() } - .toMap() - - return AvailableToAddData( - availableToAddWallets = availableToAddWallets, - ) - } - - private fun UserWallet.filteredAvailableNetworks(networks: Set) = - filterAvailableNetworksForWalletUseCase( - userWalletId = this.walletId, - networks = networks, - ) - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - marketParams: TokenMarketParams, - account: Account, - ): CryptoCurrency? { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = marketParams, - network = network, - accountIndex = derivationIndex, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt deleted file mode 100644 index 39e8de3e0b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.impl.DefaultAddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.impl.ui.DefaultAddToPortfolioManager -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal interface AddToPortfolioComponentModule { - - @Binds - fun bindAddToPortfolioComponent(factory: DefaultAddToPortfolioComponent.Factory): AddToPortfolioComponent.Factory - - @Binds - fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt deleted file mode 100644 index b093d471f6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface AddToPortfolioModelModule { - - @Binds - @IntoMap - @ClassKey(AddTokenModel::class) - fun addTokenModel(model: AddTokenModel): Model - - @Binds - @IntoMap - @ClassKey(AddToPortfolioModel::class) - fun addToPortfolioModel(model: AddToPortfolioModel): Model - - @Binds - @IntoMap - @ClassKey(TokenActionsModel::class) - fun tokenActionsModel(model: TokenActionsModel): Model - - @Binds - @IntoMap - @ClassKey(ChooseNetworkModel::class) - fun chooseNetworkModel(model: ChooseNetworkModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt deleted file mode 100644 index 71b3634b3d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.popToFirst -import com.arkivanov.decompose.router.stack.pushNew -import com.arkivanov.decompose.router.stack.replaceAll -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.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.account.PortfolioSelectorController -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.* -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter.Companion.toQuickActions -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -private const val TOKEN_ACTIONS_DELAY = 500L - -@ModelScoped -@Suppress("LongParameterList") -internal class AddToPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val callbackDelegate: AddToPortfolioCallbackDelegate, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val messageSender: UiMessageSender, - private val analyticsEventHandler: AnalyticsEventHandler, - val portfolioSelectorController: PortfolioSelectorController, -) : Model(), - ChooseNetworkComponent.Callbacks by callbackDelegate, - TokenActionsComponent.Callbacks by callbackDelegate, - AddTokenComponent.Callbacks by callbackDelegate { - - private val params = paramsContainer.require() - val navigation = StackNavigation() - var currentStack = listOf(AddToPortfolioRoutes.Empty) - - /* Flows that hold state and provide it to child models */ - val selectedNetwork: MutableSharedFlow = replayMutableSharedFlow() - val selectedPortfolio: MutableSharedFlow = replayMutableSharedFlow() - val tokenActionsData: MutableSharedFlow = replayMutableSharedFlow() - - private val addToPortfolioManager = params.addToPortfolioManager - val portfolioFetcher = addToPortfolioManager.portfolioFetcher - val eventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = addToPortfolioManager.token, - source = addToPortfolioManager.analyticsParams?.source, - ) - - val featureData: Flow = combineFeatureData() - - init { - navigation.subscribe { currentStack = it.transformer.invoke(currentStack) } - startAddToPortfolioFlow() - } - - private fun replayMutableSharedFlow() = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - @Suppress("LongMethod") - private fun startAddToPortfolioFlow() { - channelFlow { - fun finishFlow() { - params.callback.onDismiss() - channel.close() - } - val featureDataFlow: StateFlow = featureData - .filterIsInstance() - .map { it.availableToAddData } - .distinctUntilChanged() - .stateIn(this) - val isAccountMode = portfolioSelectorController.isAccountModeSync() - - // use snapshot data, looks like we don’t need to remap at runtime - val data = featureDataFlow.value - - // you must control it via [AddToPortfolioManager.state] - if (!data.isAvailableToAdd) { - finishFlow() - return@channelFlow - } - - // init data flows, emits on user/code selection, updates state holder - val firstSelectedPortfolio = setupPortfolioFlow(data) - .onEach { selectedPortfolio.emit(it) } - val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio) - .onEach { selectedNetwork.emit(it) } - - val isSinglePortfolio = data.isSinglePortfolio - if (isSinglePortfolio) { - val accountId = data.availableToAddWallets.values.first() - .availableToAddAccounts.values.first() - .account.account.accountId - // force select a portfolio, triggers [selectedPortfolio] - portfolioSelectorController.selectAccount(accountId) - } else { - logAccountSelector(isAccountMode) - navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector) - } - - val firstPartOfNavigation: Job = firstSelectedPortfolio - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - when { - // force select a network, triggers [selectedNetwork] - isSingleAvailableNetwork -> { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } - // it's important to control root screen, UI depends on it(close/arrow icon) - isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) - else -> navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - .launchIn(this) - - // main flow that combine all require data - val allRequireForAdd = combine( - flow = firstSelectedNetwork, - flow2 = firstSelectedPortfolio, - transform = { a, b -> a to b }, - ) - - // suspend until all required data is selected - 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 - // handle actions from AddToken screen - callbackDelegate.onChangeNetworkClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changeNetworkNavigationFlow() - .launchIn(this) - val route = routeToNetworkSelector(selectedPortfolio.first()) - navigation.pushNew(route) - } - .launchIn(this) - // handle actions from AddToken screen - callbackDelegate.onChangePortfolioClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) - logAccountSelector(isAccountMode) - navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) - } - .launchIn(this) - - // suspend until token is added - val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() - middleNavigationJob?.cancel() - val selectedPortfolio = selectedPortfolio.first() - - messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) - - setupTokenActionsFlow(selectedPortfolio, addedToken) - .onEach { cryptoCurrencyData -> - tokenActionsData.emit(cryptoCurrencyData) - navigation.replaceAll(AddToPortfolioRoutes.TokenActions) - } - .onEmpty { finishFlow() } - .launchIn(this) - - callbackDelegate.onLaterClick.receiveAsFlow().first() - finishFlow() - } - .catch { error -> - Timber.e(error) - params.callback.onDismiss() - } - .launchIn(modelScope) - } - - private fun logAccountSelector(isAccountMode: Boolean) { - if (isAccountMode) { - analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) - } - } - - private fun changeNetworkNavigationFlow(): Flow { - return setupNetworkFlow(selectedPortfolio) - .onEach { newNetwork -> - selectedNetwork.emit(newNetwork) - navigation.popToFirst() - } - } - - private fun changePortfolioNavigationFlow(data: AvailableToAddData): Flow = flow { - val selectedPortfolioValue = selectedPortfolio.first() - val selectedAccount = selectedPortfolioValue.account.account.account.accountId - portfolioSelectorController.selectAccount(selectedAccount) - val changedPortfolio = setupPortfolioFlow(data) - .drop(1) - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - if (isSingleAvailableNetwork) { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } else { - navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - val changedNetwork = setupNetworkFlow(changedPortfolio) - combine( - flow = changedPortfolio, - flow2 = changedNetwork, - transform = { newPortfolio, newNetwork -> - selectedPortfolio.tryEmit(newPortfolio) - selectedNetwork.tryEmit(newNetwork) - navigation.popToFirst() - }, - ).collect { emit(it) } - } - - private fun setupTokenActionsFlow( - selectedPortfolio: SelectedPortfolio, - addedToken: CryptoCurrencyStatus, - ): Flow { - val timeFlow = channelFlow { - val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) } - getCryptoCurrencyActionsUseCase( - currency = addedToken.currency, - accountId = selectedPortfolio.account.account.account.accountId, - ).onEach { state -> - val requestedQuickActions = toQuickActions(state.states) - when { - requestedQuickActions.isNotEmpty() -> { - timerJob.cancel() - send(state) - } - // wait any requestedQuickActions while timer active - timerJob.isActive -> Unit - else -> close() - } - }.collect() - } - return timeFlow.map { actionsState -> - PortfolioData.CryptoCurrencyData( - userWallet = selectedPortfolio.userWallet, - status = actionsState.cryptoCurrencyStatus, - actions = actionsState.states, - ) - } - } - - private fun setupPortfolioFlow(data: AvailableToAddData): Flow = combine( - flow = portfolioSelectorController.isAccountMode, - flow2 = portfolioSelectorController.selectedAccount, - transform = { isAccountMode, selectedAccountId -> - selectedAccountId ?: return@combine null - val availableToAddWallets = - data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null - val availableToAddAccount = - availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null - if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged()) - SelectedPortfolio( - isAccountMode = isAccountMode, - userWallet = availableToAddWallets.userWallet, - account = availableToAddAccount, - hasMorePortfoliosAvailable = !data.isSinglePortfolio, - ) - }, - ) - .filterNotNull() - - private fun setupNetworkFlow(selectedPortfolioFlow: Flow): Flow = combine( - flow = selectedPortfolioFlow, - flow2 = callbackDelegate.onNetworkSelected.receiveAsFlow(), - transform = transform@{ selectedPortfolio, selectedNetwork -> - SelectedNetwork( - cryptoCurrency = createCryptoCurrency( - userWallet = selectedPortfolio.userWallet, - network = selectedNetwork, - account = selectedPortfolio.account, - ) ?: return@transform null, - selectedNetwork = selectedNetwork, - hasMoreNetworksAvailable = !selectedPortfolio.account.isSingleNetwork, - ) - }, - ) - .filterNotNull() - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - account: AvailableToAddAccount, - ): CryptoCurrency? { - val accountIndex = when (account.account) { - is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = addToPortfolioManager.token, - network = network, - accountIndex = accountIndex, - ) - } - - private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector { - return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio) - } - - private fun combineFeatureData() = addToPortfolioManager.state.onEach { state -> - when (state) { - is AddToPortfolioManager.State.AvailableToAdd -> - portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> - val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] - ?: return@isEnabled false - val isAvailableAccount = - availableWallet.availableToAddAccounts[accountStatus.account.accountId] - ?.isAvailableToAdd == true - return@isEnabled isAvailableAccount - } - AddToPortfolioManager.State.Init, - AddToPortfolioManager.State.NothingToAdd, - -> Unit - } - } -} - -@ModelScoped -internal class AddToPortfolioCallbackDelegate @Inject constructor() : - ChooseNetworkComponent.Callbacks, - TokenActionsComponent.Callbacks, - AddTokenComponent.Callbacks { - - val onNetworkSelected = Channel() - val onLaterClick = Channel() - val onChangeNetworkClick = Channel() - val onChangePortfolioClick = Channel() - val onTokenAdded = Channel() - - override fun onNetworkSelected(network: TokenMarketInfo.Network) { - onNetworkSelected.trySend(network) - } - - override fun onLaterClick() { - onLaterClick.trySend(Unit) - } - - override fun onChangeNetworkClick() { - onChangeNetworkClick.trySend(Unit) - } - - override fun onChangePortfolioClick() { - onChangePortfolioClick.trySend(Unit) - } - - override fun onTokenAdded(status: CryptoCurrencyStatus) { - onTokenAdded.trySend(status) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt deleted file mode 100644 index 4f46fac1d3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.navigation.Route -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import kotlinx.serialization.Serializable - -@Serializable -@Immutable -internal sealed interface AddToPortfolioRoutes : Route { - - @Serializable - data object Empty : AddToPortfolioRoutes - - @Serializable - data object PortfolioSelector : AddToPortfolioRoutes - - @Serializable - data class NetworkSelector( - val selectedPortfolio: SelectedPortfolio, - ) : AddToPortfolioRoutes - - @Serializable - data object AddToken : AddToPortfolioRoutes - - @Serializable - data object TokenActions : AddToPortfolioRoutes -} \ No newline at end of file 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 deleted file mode 100644 index be45fedb3a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.addtoken.AddTokenUM -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.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase -import com.tangem.domain.models.account.Account -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -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 -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class AddTokenModel @Inject constructor( - paramsContainer: ParamsContainer, - private val uiBuilder: AddTokenUiBuilder, - private val messageSender: UiMessageSender, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder = params.eventBuilder - private val addTokenJob = JobHolder() - - val uiState: StateFlow - field = MutableStateFlow(value = null) - - init { - combine( - flow = params.selectedNetwork.distinctUntilChanged(), - flow2 = params.selectedPortfolio.distinctUntilChanged(), - transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.join() - val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) - uiBuilder.updateContent( - selectedPortfolio = selectedPortfolio, - selectedNetwork = selectedNetwork, - isTangemIconVisible = isTangemIconVisible, - onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) }, - ) - }, - ) - .onEach { newUI -> uiState.value = newUI } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } - - private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) = - modelScope.launch(dispatchers.default) { - val um = uiState.value ?: return@launch - - val cryptoCurrency = selectedNetwork.cryptoCurrency - val account = selectedPortfolio.account.account.account - val accountId = account.accountId - val isMainNetwork = selectedNetwork.selectedNetwork.contractAddress == null - - val unsupportedCurrency = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = accountId.userWalletId, - rawNetworkId = selectedNetwork.selectedNetwork.networkId, - isMainNetwork = isMainNetwork, - ) - - if (unsupportedCurrency != null) return@launch - - uiState.value = um.toggleProgress(true) - 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 -> - processError(error = error) - uiState.value = um.toggleProgress(false) - return@launch - } - - val status = getAccountCurrencyStatusUseCase( - userWalletId = accountId.userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ).firstOrNull() - if (status == null) { - processError(error = null) - } else { - when (account) { - is Account.CryptoPortfolio -> if (!account.isMainAccount) { - analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount()) - } - 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) - } - - private suspend fun needColdWalletInteraction( - selectedNetwork: SelectedNetwork, - selectedPortfolio: SelectedPortfolio, - ): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedPortfolio.userWallet.walletId, - networksWithDerivationPath = mapOf( - selectedNetwork.selectedNetwork.networkId to selectedNetwork.cryptoCurrency.network.derivationPath.value, - ), - ) - - private fun processError(error: Throwable?) { - val message = error?.message?.let { stringReference(it) } - ?: resourceReference(R.string.common_something_went_wrong) - messageSender.send(ToastMessage(message = message)) - } -} \ No newline at end of file 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 deleted file mode 100644 index ec88d88216..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.account.AccountIconUM -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.addtoken.AddTokenUM -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -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.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.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import javax.inject.Inject - -@ModelScoped -internal class AddTokenUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, -) { - private val params = paramsContainer.require() - - private fun createNetwork(selectedNetwork: SelectedNetwork): AddTokenUM.Network { - return AddTokenUM.Network( - icon = selectedNetwork.cryptoCurrency.network.iconResId, - name = stringReference(selectedNetwork.cryptoCurrency.network.name), - editable = selectedNetwork.hasMoreNetworksAvailable, - onClick = { params.callbacks.onChangeNetworkClick() }, - ) - } - - private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM? - val portfolioName: TextReference - when (selectedPortfolio.isAccountMode) { - false -> { - accountIcon = null - portfolioName = stringReference(selectedPortfolio.userWallet.name) - } - true -> { - val accountStatus = selectedPortfolio.account.account - portfolioName = accountStatus.account.accountName.toUM().value - accountIcon = when (accountStatus) { - is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) - is Payment -> AccountIconUM.Payment - } - } - } - return PortfolioSelectUM( - icon = accountIcon, - name = portfolioName, - isAccountMode = selectedPortfolio.isAccountMode, - isMultiChoice = selectedPortfolio.hasMorePortfoliosAvailable, - onClick = { params.callbacks.onChangePortfolioClick() }, - ) - } - - fun updateContent( - selectedPortfolio: SelectedPortfolio, - selectedNetwork: SelectedNetwork, - isTangemIconVisible: Boolean, - onConfirmClick: () -> Unit, - ): AddTokenUM { - // its may happens when change portfolio after selected both params in line navigation - val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks - .any { selectedNetwork.selectedNetwork.networkId == it.networkId } - val button = AddTokenUM.Button( - isEnabled = isAvailableNetwork, - showProgress = false, - isTangemIconVisible = isTangemIconVisible, - text = resourceReference(R.string.common_add), - onConfirmClick = onConfirmClick, - ) - val networkUM = createNetwork(selectedNetwork) - val portfolioUM = createPortfolio(selectedPortfolio) - val currency = selectedNetwork.cryptoCurrency - val tokenToAdd = TokenItemState.Content( - id = currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(currency), - titleState = TokenItemState.TitleState.Content(stringReference(currency.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return AddTokenUM( - tokenToAdd = tokenToAdd, - network = networkUM, - portfolio = portfolioUM, - button = button, - ) - } - - companion object { - - fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy( - button = this.button.copy(showProgress = showProgress), - ) - } -} \ No newline at end of file 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 deleted file mode 100644 index f6d5e737b6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import arrow.core.getOrElse -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.core.ui.message.SnackbarMessage -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.impl.R -import timber.log.Timber -import javax.inject.Inject - -class CheckCurrencyUnsupportedDelegate @Inject constructor( - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val messageSender: UiMessageSender, -) { - - suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - val result = checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { throwable -> - Timber.e( - throwable, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = throwable.localizedMessage?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - - if (result != null) { - showUnsupportedWarning(result) - } - return result - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt deleted file mode 100644 index 4834f7cbbb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -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.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import com.tangem.features.markets.portfolio.impl.model.BlockchainRowUMConverter -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class ChooseNetworkModel @Inject constructor( - paramsContainer: ParamsContainer, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - val uiState: StateFlow = MutableStateFlow(buildUI()) - - private fun buildUI(): ChooseNetworkUM { - val allAvailable = params.selectedPortfolio.account.availableNetworks - val alreadyAdded = allAvailable - .subtract(params.selectedPortfolio.account.availableToAddNetworks) - val converter = BlockchainRowUMConverter( - alreadyAddedNetworks = alreadyAdded.mapTo(mutableSetOf()) { it.networkId }, - ) - val allAvailableNetworks = allAvailable.map { it to true } - return ChooseNetworkUM( - networks = converter.convertList(allAvailableNetworks).toPersistentList(), - onNetworkClick = onNetworkClick@{ row -> - val network = allAvailable - .find { it.networkId == row.id } - ?: return@onNetworkClick - checkNetwork(row, network) - }, - ) - } - - private fun checkNetwork(row: BlockchainRowUM, network: TokenMarketInfo.Network) = modelScope.launch { - val selectedWalletId = params.selectedPortfolio.userWallet.walletId - val unsupportedState = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = row.id, - isMainNetwork = row.isMainNetwork, - ) - if (unsupportedState == null) { - params.callbacks.onNetworkSelected(network) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt deleted file mode 100644 index 466befc011..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -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.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler.HandledQuickAction -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class TokenActionsModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val uiBuilder: TokenActionsUiBuilder, - private val analyticsEventHandler: AnalyticsEventHandler, - private val receiveAddressesFactory: ReceiveAddressesFactory, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder get() = params.eventBuilder - private val currentAppCurrency = getSelectedAppCurrencyUseCase.invokeOrDefault() - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler: TokenActionsHandler = - tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) }, - ) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val uiState: StateFlow = params.data - .mapLatest { uiBuilder.build(it, tokenActionsHandler) } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - - private fun handledQuickAction(handledAction: HandledQuickAction) { - val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action) - analyticsEventHandler.send(event) - val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive - if (!isReceive) return - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = handledAction.cryptoCurrencyData.status, - userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(tokenConfig) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt deleted file mode 100644 index 6a1dcb993e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -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.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import javax.inject.Inject - -@ModelScoped -internal class TokenActionsUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, - private val analyticsEventHandler: AnalyticsEventHandler, -) { - private val params = paramsContainer.require() - - fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM { - val status = data.status - val tokenUM = TokenItemState.Content( - id = status.currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(status.currency), - titleState = TokenItemState.TitleState.Content(stringReference(status.currency.name)), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(status.currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return TokenActionsUM( - token = tokenUM, - onLaterClick = { - analyticsEventHandler.send(params.eventBuilder.getTokenLater()) - params.callbacks.onLaterClick() - }, - quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt deleted file mode 100644 index e2f26ae35b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -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.util.fastForEachIndexed -import com.tangem.core.ui.components.label.Label -import com.tangem.core.ui.components.label.entity.LabelStyle -import com.tangem.core.ui.components.label.entity.LabelUM -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -private const val DISABLED_ALPHA = 0.4f - -@Composable -internal fun ChooseNetworkContent(state: ChooseNetworkUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.networks.fastForEachIndexed { index, model -> - key(model.id) { - BlockchainRow( - model = model, - itemPadding = PaddingValues( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing14, - ), - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = model.isEnabled, onClick = { state.onNetworkClick(model) }), - ) { - if (!model.isEnabled) { - Label( - modifier = Modifier.alpha(DISABLED_ALPHA), - state = LabelUM( - text = resourceReference(R.string.common_added), - style = LabelStyle.REGULAR, - ), - ) - } - } - } - } - } -} - -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview(@PreviewParameter(ChooseNetworkContentProvider::class) content: ChooseNetworkUM) { - TangemThemePreview { - ChooseNetworkContent( - state = content, - ) - } -} - -internal class ChooseNetworkContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = UUID.randomUUID().toString(), - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.img_eth_22, - isMainNetwork = false, - isSelected = true, - isEnabled = true, - ) - - override val values: Sequence - get() = sequenceOf( - ChooseNetworkUM( - onNetworkClick = {}, - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - ), - blockchainRow.copy( - iconResId = R.drawable.ic_bsc_16, - isEnabled = false, - ), - blockchainRow.copy(iconResId = R.drawable.img_polygon_22), - blockchainRow.copy(iconResId = R.drawable.img_optimism_22), - ), - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt deleted file mode 100644 index e396ef008e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager.State -import com.tangem.features.markets.portfolio.add.impl.converter.AvailableToAddDataConverter -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -internal class DefaultAddToPortfolioManager @AssistedInject constructor( - private val availableToAddDataConverter: AvailableToAddDataConverter, - @Assisted override val token: TokenMarketParams, - @Assisted override val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - @Assisted val scope: CoroutineScope, - dispatchers: CoroutineDispatcherProvider, - portfolioFetcherFactory: PortfolioFetcher.Factory, -) : AddToPortfolioManager { - - private val _allAvailableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val allAvailableNetworks: Flow> = _allAvailableNetworks.asSharedFlow() - override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), - scope = scope, - ) - - override val state: StateFlow = - combine( - flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(), - flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(), - ) { balances, availableNetworks -> - val data = availableToAddDataConverter.convert( - balances = balances, - availableNetworks = availableNetworks, - marketParams = token, - ) - if (data.isAvailableToAdd) { - State.AvailableToAdd(data) - } else { - State.NothingToAdd - } - } - .flowOn(dispatchers.default) - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = State.Init, - ) - - override fun setTokenNetworks(networks: List) { - _allAvailableNetworks.tryEmit(networks) - } - - @AssistedFactory - interface Factory : AddToPortfolioManager.Factory { - override fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - ): DefaultAddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt deleted file mode 100644 index e0b24bf672..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.vector.ImageVector -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 androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -@Composable -internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - ) { - TokenItem( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action), - state = state.token, - isBalanceHidden = false, - ) - - SpacerH(TangemTheme.dimens.spacing14) - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.quickActions.actions.fastForEach { action -> - key(action.title) { - ActionRow( - state = action, - onClick = { state.quickActions.onQuickActionClick(action) }, - onLongClick = { state.quickActions.onQuickActionLongClick(action) }, - ) - } - } - } - - SpacerH16() - - SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_later), - onClick = state.onLaterClick, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun ActionRow( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit), - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal.takeIf { state.isLongClickAvailable }, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing15), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - val containerColor = TangemTheme.colors.background.action - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), - shape = CircleShape, - ) - .size(36.dp) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(TokenActionsContentPreviewProvider::class) state: TokenActionsUM) { - TangemThemePreview { - TokenActionsContent( - state = state, - ) - } -} - -private class TokenActionsContentPreviewProvider : PreviewParameterProvider { - private val tokenState - get() = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_eth_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Tether"), - ), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")), - onItemClick = {}, - onItemLongClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - TokenActionsUM( - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - token = tokenState, - onLaterClick = {}, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt deleted file mode 100644 index 8e27218757..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -data class ChooseNetworkUM( - val networks: ImmutableList, - val onNetworkClick: (BlockchainRowUM) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt deleted file mode 100644 index cb2466e02a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM - -internal data class TokenActionsUM( - val token: TokenItemState, - val quickActions: PortfolioTokenUM.QuickActions, - val onLaterClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt deleted file mode 100644 index 62babbdc62..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.markets.portfolio.api - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsPortfolioComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val source: String, - ) - - fun setTokenNetworks(networks: List) - - fun setNoNetworksAvailable() - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt deleted file mode 100644 index 200d6916bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.markets.portfolio.impl - -import androidx.compose.runtime.Composable -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.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioRoute -import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: MarketsPortfolioComponent.Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, -) : AppComponentContext by context, MarketsPortfolioComponent { - - private val model: MarketsPortfolioModel = getOrCreateModel(params) - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = MarketsPortfolioRoute.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - override fun setTokenNetworks(networks: List) { - model.setTokenNetworks(networks) - } - - override fun setNoNetworksAvailable() { - model.setNoNetworksAvailable() - } - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - MyPortfolio(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: MarketsPortfolioRoute, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create( - context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = requireNotNull(model.newAddToPortfolioManager) { - "newAddToPortfolioManager must be initialized" - }, - callback = model.addToPortfolioCallback, - ), - ) - is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config.config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - } - - @AssistedFactory - interface Factory : MarketsPortfolioComponent.Factory { - override fun create( - context: AppComponentContext, - params: MarketsPortfolioComponent.Params, - ): DefaultMarketsPortfolioComponent - } -} \ No newline at end of file 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 deleted file mode 100644 index 2658bb0659..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM - -internal class PortfolioAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - val source: String?, - ) { - - fun addToPortfolioClicked() = PortfolioAnalyticsEvent( - event = "Button - Add To Portfolio", - 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 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 = 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) = - PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Button - Buy" - TokenActionsBSContentUM.Action.Receive -> "Button - Receive" - TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" - TokenActionsBSContentUM.Action.Stake -> "Button - Stake" - TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode" - else -> "error" - }, - params = buildMap { - put("Token", token.symbol) - if (source != null) put("Source", source) - put("blockchain", blockchainName) - }, - ) - - fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy" - TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive" - TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange" - 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/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt deleted file mode 100644 index d011fbf799..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent -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 bindMarketsPortfolioComponent( - factory: DefaultMarketsPortfolioComponent.Factory, - ): MarketsPortfolioComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt deleted file mode 100644 index 2b35fe3c10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -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(MarketsPortfolioModel::class) - fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt deleted file mode 100644 index ac34715c4d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import com.tangem.domain.appcurrency.model.AppCurrency -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.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenActionsState - -/** - * Portfolio data. Combined data from all flows that required to setup portfolio - * - * @property walletsWithCurrencies wallets with crypto currency statuses - * @property appCurrency app currency - * @property isBalanceHidden flag that indicates if balance should be hidden - * @property walletsWithBalance wallets with total balance - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioData( - val walletsWithCurrencies: Map>, - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val walletsWithBalance: Map>, -) { - data class CryptoCurrencyData( - val userWallet: UserWallet, - val status: CryptoCurrencyStatus, - val actions: List, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt deleted file mode 100644 index 0dadd3bcf4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Loader of portfolio data - * - * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses - * @property getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings - * @property getWalletTotalBalanceUseCase use case for getting wallet total balance - * -[REDACTED_AUTHOR] - */ -internal class PortfolioDataLoader @Inject constructor( - private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) { - - /** Load data by [currencyRawId] */ - @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: CryptoCurrency.RawID): Flow { - return combine( - flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> - PortfolioData( - walletsWithCurrencies = walletsWithCurrencies, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - walletsWithBalance = emptyMap(), - ) - } - // setup balances for wallets from walletsWithCurrencyStatuses - .flatMapLatest { portfolioData -> - getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), - ) - .map { portfolioData.copy(walletsWithBalance = it) } - .onEmpty { emit(portfolioData) } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) - .distinctUntilChanged() - .map { walletsWithMaybeStatuses -> - walletsWithMaybeStatuses.mapValues { entry -> - entry.value.mapNotNull { it.getOrNull() } - } - } - .flatMapLatest { walletsWithStatuses -> - val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> - statuses.map { status -> - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { - YieldSupplyAvailability.Unavailable - } - getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) - .map { actionStates -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = actionStates.states, - ) - } - } - } - - combine(actionsFlows) { actions -> - walletsWithStatuses.mapValues { entry -> - entry.value.mapNotNull { status -> - actions.firstOrNull { data -> - data.userWallet == entry.key && data.status == status - } - } - } - }.onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { status -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = emptyList(), - ) - } - }, - ) - } - }.onEmpty { - emit(emptyMap()) - } - .distinctUntilChanged() - } - - private fun getWalletsWithTotalBalanceFlow( - ids: List, - ): Flow>> { - return combine( - flows = ids - .map { userWalletId -> - getWalletTotalBalanceUseCase(userWalletId) - .map { userWalletId to it } - .distinctUntilChanged() - }, - transform = { it.toMap() }, - ) - .distinctUntilChanged() - .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt deleted file mode 100644 index bf294a2895..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.toImmutableList - -/** - * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] - * - * @property token token params - * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed - * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed - * @property onNetworkSwitchClick callback is invoked when network switch is clicked - * @property onAnotherWalletSelect callback is invoked when wallet is selected - * @property onContinueClick callback is invoked when continue button is clicked - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class AddToPortfolioBSContentUMFactory( - private val addToPortfolioManager: AddToPortfolioManager, - private val token: TokenMarketParams, - private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, - private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, - private val onAnotherWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, -) { - - /** - * Create [TangemBottomSheetConfig] - * - - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param alreadyAddedNetworks already added networks - */ - @Suppress("LongParameterList") - fun create( - currentState: TangemBottomSheetConfig?, - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - selectedWallet: UserWallet?, - alreadyAddedNetworks: Set?, - artworks: Map, - ): TangemBottomSheetConfig { - return (currentState ?: TangemBottomSheetConfig.Empty).copy( - isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible, - onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, - content = if (selectedWallet != null && alreadyAddedNetworks != null) { - AddToPortfolioBSContentUM( - selectedWallet = selectedWallet.toSelectedUserWalletItemUM( - portfolioData = portfolioData, - balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(), - artwork = artworks[selectedWallet.walletId], - ), - selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = addToPortfolioManager.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - addToPortfolioData = portfolioUIData.addToPortfolioData, - ), - alreadyAddedNetworks = alreadyAddedNetworks, - onNetworkSwitchClick = onNetworkSwitchClick, - ).convert(value = token), - isScanCardNotificationVisible = portfolioUIData.shouldRequireColdWalletInteraction, - isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks( - userWalletId = selectedWallet.walletId, - ), - onContinueButtonClick = { - onContinueClick( - selectedWallet.walletId, - portfolioUIData.addToPortfolioData.getAddedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - ) - }, - walletSelectorConfig = createWalletSelectorBSConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible, - portfolioData = portfolioData, - selectedWalletId = selectedWallet.walletId, - artworks = artworks, - ), - isWalletBlockVisible = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency).size > 1, - ) - } else { - TangemBottomSheetConfigContent.Empty - }, - ) - } - - private fun UserWallet.toSelectedUserWalletItemUM( - artwork: UserWalletItemUM.ImageState? = null, - portfolioData: PortfolioData, - balance: TotalFiatBalance?, - ): UserWalletItemUM { - return UserWalletItemUMConverter( - onClick = { onWalletSelectorVisibilityChange(true) }, - endIcon = UserWalletItemUM.EndIcon.Arrow, - balance = balance, - artwork = artwork, - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - ).convert(value = this) - } - - private fun createWalletSelectorBSConfig( - isShow: Boolean, - portfolioData: PortfolioData, - selectedWalletId: UserWalletId, - artworks: Map, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShow, - onDismissRequest = { onWalletSelectorVisibilityChange(false) }, - content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency) - .map { it.key } - .map { userWallet -> - val balance = portfolioData.walletsWithBalance[userWallet.walletId] - - UserWalletItemUMConverter( - onClick = { walletId -> - if (walletId != selectedWalletId) { - onAnotherWalletSelect(walletId) - onWalletSelectorVisibilityChange(false) - } - }, - appCurrency = portfolioData.appCurrency, - balance = balance?.getOrNull(), - isBalanceHidden = portfolioData.isBalanceHidden, - endIcon = if (userWallet.walletId == selectedWalletId) { - UserWalletItemUM.EndIcon.Checkmark - } else { - UserWalletItemUM.EndIcon.None - }, - artwork = artworks[userWallet.walletId], - ).convert(userWallet) - } - .toImmutableList(), - onBack = { onWalletSelectorVisibilityChange(false) }, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt deleted file mode 100644 index 15718128de..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import timber.log.Timber -import javax.inject.Inject - -internal typealias WalletsWithNetworks = Map> - -/** - * Manager for tracking changing networks in AddToPortfolio - * -[REDACTED_AUTHOR] - */ -internal class AddToPortfolioManager @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, -) { - - val availableNetworks = MutableStateFlow?>(value = null) - private val addedNetworks = MutableStateFlow(value = emptyMap()) - private val removedNetworks = MutableStateFlow(value = emptyMap()) - - /** Get [AddToPortfolioData] as flow */ - fun getAddToPortfolioData(): Flow { - return combine( - flow = availableNetworks, - flow2 = addedNetworks, - flow3 = removedNetworks, - transform = ::AddToPortfolioData, - ) - } - - /** Set available networks [networks] */ - fun setAvailableNetworks(networks: List) { - availableNetworks.value = networks.toSet() - } - - /** Add network [networkId] to [userWalletId] */ - fun addNetwork(userWalletId: UserWalletId, networkId: String) { - addedNetworks.add(userWalletId, networkId) - - removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) - } - - /** Remove network [networkId] from [userWalletId] */ - fun removeNetwork(userWalletId: UserWalletId, networkId: String) { - removedNetworks.add(userWalletId, networkId) - - addedNetworks.cancelPrevChangeIfExist( - userWalletId = userWalletId, - networkId = networkId, - ) - } - - /** Remove all networks by [userWalletId] */ - fun removeAllChanges(userWalletId: UserWalletId) { - addedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - - removedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - } - - fun associateWithToggle( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - addToPortfolioData: AddToPortfolioData, - ): Map { - val filteredNetworks = filterAvailableNetworksForWalletUseCase( - userWalletId = userWalletId, - networks = addToPortfolioData.availableNetworks.orEmpty(), - ) - // Use user choice or check already added networks - return filteredNetworks.associateWith { availableNetwork -> - val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork) - - if (isAddedByUser == true) return@associateWith true - - val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork) - - if (isRemovedByUser == true) return@associateWith false - - val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } - - isAddedBefore - } - } - - private fun MutableStateFlow.cancelPrevChangeIfExist( - userWalletId: UserWalletId, - networkId: String, - ) { - if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) - } - - private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) - } - - private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) - } - - private fun MutableStateFlow.change( - userWalletId: UserWalletId, - networkId: String, - isAddAction: Boolean, - ) { - val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } - - if (network == null) { - Timber.d( - "Network [$networkId] doesn't contain in available networks [%s]", - availableNetworks.value?.joinToString { it.networkId }, - ) - - return - } - - update { currentMap -> - currentMap.toMutableMap().apply { - this[userWalletId] = if (isAddAction) { - this[userWalletId].orEmpty() + network - } else { - this[userWalletId].orEmpty() - network - } - } - } - } - - /** - * Add to portfolio data - * - * @property availableNetworks available networks that user can add to portfolio - * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet - * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet - * - * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just - * toggle it. But when we will save user changes, we will check what tokens have already been added or - * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] - */ - data class AddToPortfolioData( - val availableNetworks: Set?, - val addedNetworks: WalletsWithNetworks, - val removedNetworks: WalletsWithNetworks, - ) { - - fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() || - removedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ - fun getAddedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() - - return addedNetworksByUser.map { it.networkId } - .minus(alreadyAddedNetworkIds) - .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - - /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ - fun getRemovedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() - - return alreadyAddedNetworkIds - .minus(removedNetworksByUser.map { it.networkId }.toSet()) - .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt deleted file mode 100644 index c9ec3f67cb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] - * - * @property alreadyAddedNetworks set of already added networks - * -[REDACTED_AUTHOR] - */ -internal class BlockchainRowUMConverter( - private val alreadyAddedNetworks: Set, -) : Converter, BlockchainRowUM> { - - override fun convert(value: Pair): BlockchainRowUM { - val (network, isSelected) = value - - val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) - ?: error("Can't find blockchain info for ${network.networkId}") - - val isMainNetwork = network.contractAddress == null - - val isEnabled = !alreadyAddedNetworks.contains(network.networkId) - - return BlockchainRowUM( - id = network.networkId, - name = blockchainInfo.name, - type = getNetworkType(network, blockchainInfo), - iconResId = if (isEnabled) { - if (isSelected) { - getActiveIconRes(blockchainInfo.blockchainId) - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - } - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - }, - isMainNetwork = isMainNetwork, - isSelected = isSelected, - isEnabled = isEnabled, - ) - } - - private fun getNetworkType( - network: TokenMarketInfo.Network, - blockchainInfo: BlockchainUtils.BlockchainInfo, - ): String { - val isMainNetwork = network.contractAddress == null - return when { - BlockchainUtils.isL2Network(networkId = network.networkId) -> MAIN_NETWORK_L2_TYPE_NAME - isMainNetwork -> MAIN_NETWORK_TYPE_NAME - else -> blockchainInfo.protocolName - } - } - - private companion object { - const val MAIN_NETWORK_TYPE_NAME = "MAIN" - const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2" - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt deleted file mode 100644 index 0157de78d9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ /dev/null @@ -1,426 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -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.ui.userwallet.state.UserWalletItemUM -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.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.markets.SaveMarketTokensUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.wallet.utils.UserWalletImageFetcher -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.operations.attestation.ArtworkSize -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager as NewAddToPortfolioManager - -@Suppress("LongParameterList", "LargeClass") -@Stable -@ModelScoped -internal class MarketsPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val messageSender: UiMessageSender, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val portfolioDataLoader: PortfolioDataLoader, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val addToPortfolioManager: AddToPortfolioManager, - private val analyticsEventHandler: AnalyticsEventHandler, - private val userWalletImageFetcher: UserWalletImageFetcher, - private val receiveAddressesFactory: ReceiveAddressesFactory, - accountsFeatureToggles: AccountsFeatureToggles, - newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory, - newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory, -) : Model() { - - private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - val state: StateFlow get() = _state - - private val params = paramsContainer.require() - private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = params.token, - source = params.analyticsParams?.source, - ) - - val newAddToPortfolioManager: NewAddToPortfolioManager? - val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate? - - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ - private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - } - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler = tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> - val currencyNetwork = handledAction.cryptoCurrencyData.status.currency.network - analyticsEventHandler.send( - analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = currencyNetwork.name, - ), - ) - configureReceiveAddresses(handledAction) - }, - ) - - private val factory = MyPortfolioUMFactory( - onAddClick = { - onAddToPortfolioBSVisibilityChange(isShow = true) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioClicked(), - ) - }, - addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( - addToPortfolioManager = addToPortfolioManager, - token = params.token, - onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, - onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, - onNetworkSwitchClick = ::onNetworkSwitchClick, - onAnotherWalletSelect = { walletId -> - onWalletSelect(walletId) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioWalletChanged(), - ) - }, - onContinueClick = { selectedWalletId, addedNetworks -> - onContinueClick(selectedWalletId, addedNetworks) - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioContinue( - blockchainNames = addedNetworks.mapNotNull { - BlockchainUtils.getNetworkInfo(it.networkId)?.name - }, - ), - ) - }, - ), - currentState = Provider { _state.value }, - tokenActionsHandler = tokenActionsHandler, - updateTokens = { updateBlock -> - updateTokensState { state -> - state.copy(tokens = updateBlock(state.tokens)) - } - }, - ) - - init { - if (accountsFeatureToggles.isFeatureEnabled) { - newAddToPortfolioManager = newAddToPortfolioManagerFactory - .create( - modelScope, - params.token, - params.analyticsParams, - ) - newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create( - scope = modelScope, - token = params.token, - tokenActionsHandler = tokenActionsHandler, - buttonState = newAddToPortfolioManager.state.map { managerState -> - when (managerState) { - is NewAddToPortfolioManager.State.AvailableToAdd -> AddButtonState.Available - NewAddToPortfolioManager.State.Init -> AddButtonState.Loading - NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable - } - }, - onAddClick = { - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) - bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) - }, - ) - newMarketsPortfolioDelegate.combineData() - .onEach { _state.value = it } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - newAddToPortfolioManager = null - newMarketsPortfolioDelegate = null - // Subscribe on selected wallet flow to support actual selected wallet - subscribeOnSelectedMultiWalletUpdates() - - subscribeOnStateUpdates() - } - } - - fun setTokenNetworks(networks: List) { - addToPortfolioManager.setAvailableNetworks(networks) - newAddToPortfolioManager?.setTokenNetworks(networks) - newMarketsPortfolioDelegate?.setTokenNetworks(networks) - } - - fun setNoNetworksAvailable() { - addToPortfolioManager.setAvailableNetworks(emptyList()) - newAddToPortfolioManager?.setTokenNetworks(emptyList()) - newMarketsPortfolioDelegate?.setTokenNetworks(emptyList()) - } - - private fun subscribeOnSelectedMultiWalletUpdates() { - getSelectedWalletUseCase() - .getOrElse { e -> - Timber.e("Failed to load selected wallet: $e") - error("Failed to load selected wallet") - } - .onEach { wallet -> - selectedMultiWalletIdFlow.value = wallet.takeIf { it.isMultiCurrency }?.walletId - } - .launchIn(modelScope) - } - - private fun subscribeOnStateUpdates() { - combine( - flow = loadPortfolioDataWithArtworks(params.token.id), - flow2 = getPortfolioUIDataFlow(), - transform = { pair, portfolioUIData -> - val (portfolioData, artworks) = pair - factory.create(portfolioData, portfolioUIData, artworks) - }, - ) - .onEach { _state.value = it } - .launchIn(modelScope) - } - - private fun loadPortfolioDataWithArtworks( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - val wallets = Channel>() - val portfolioFlow = portfolioDataLoader - .load(currencyRawId) - .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - - val artworksFlow = wallets.receiveAsFlow() - .distinctUntilChanged() - .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } - - return combine( - flow = portfolioFlow, - flow2 = artworksFlow, - ) { portfolioData, artworks -> portfolioData to artworks } - } - - private fun getPortfolioUIDataFlow(): Flow { - return combine( - flow = portfolioBSVisibilityModelFlow, - flow2 = selectedMultiWalletIdFlow, - flow3 = addToPortfolioManager.getAddToPortfolioData(), - transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> - PortfolioUIData( - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, - shouldRequireColdWalletInteraction = needColdWalletInteraction( - selectedWalletId, - addToPortfolioData, - ), - ) - }, - ) - } - - private suspend fun needColdWalletInteraction( - selectedWalletId: UserWalletId?, - addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - ): Boolean { - return if (selectedWalletId != null) { - coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedWalletId, - networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() - .associate { it.networkId to null }, - ) - } else { - false - } - } - - private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { - val selectedWalletId = selectedMultiWalletIdFlow.value - - if (selectedWalletId == null) { - Timber.e("Impossible to switch network when selected wallet is null") - return - } - - if (isChecked) { - modelScope.launch { - val unsupportedState = checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = blockchainRowUM.id, - isMainNetwork = blockchainRowUM.isMainNetwork, - ) - if (unsupportedState != null) { - showUnsupportedWarning(unsupportedState) - } else { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - - private suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { error -> - Timber.e( - error, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = error.localizedMessage - ?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } - - private fun onWalletSelect(userWalletId: UserWalletId) { - selectedMultiWalletIdFlow.update { prevUserWalletId -> - prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) - - userWalletId - } - } - - private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { - modelScope.launch { - saveMarketTokensUseCase( - userWalletId = userWalletId, - tokenMarketParams = params.token, - addedNetworks = addedNetworks, - removedNetworks = emptySet(), - ) - - onAddToPortfolioBSVisibilityChange(isShow = false) - - addToPortfolioManager.removeAllChanges(userWalletId) - } - } - - private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false) - } - } - - private fun onWalletSelectorVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow) - } - } - - private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { - _state.update { stateToUpdate -> - val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate - block(tokensState) - } - } - - private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { - val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive - if (isNewReceive) { - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = quickAction.cryptoCurrencyData.status, - userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(MarketsPortfolioRoute.TokenReceive(tokenConfig)) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt deleted file mode 100644 index 576d9cda78..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.models.TokenReceiveConfig -import kotlinx.serialization.Serializable - -@Serializable -sealed interface MarketsPortfolioRoute : Route { - - @Serializable - data object AddToPortfolio : MarketsPortfolioRoute - - @Serializable - data class TokenReceive( - val config: TokenReceiveConfig, - ) : MarketsPortfolioRoute -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt deleted file mode 100644 index d5c027adb8..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList - -/** - * Factory for creating [MyPortfolioUM] - * - * @property onAddClick callback when user wants to add new token - * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content - * @property tokenActionsHandler token actions handler - * @property currentState current state provider - * @property updateTokens callback for updating tokens - * -[REDACTED_AUTHOR] - */ -internal class MyPortfolioUMFactory( - private val onAddClick: () -> Unit, - private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, - private val tokenActionsHandler: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) { - - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): MyPortfolioUM { - val addToPortfolioData = portfolioUIData.addToPortfolioData - - val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true - if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { - portfolioData.walletsWithCurrencies - } else { - portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) - } - - val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() - if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() - - return if (hasMultiWallets) { - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - ) - } else { - MyPortfolioUM.UnavailableForWallet - } - } - - return TokensPortfolioUMConverter( - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - addButtonState = walletsWithCurrencies.getAddButtonState( - availableNetworks = addToPortfolioData.availableNetworks, - ), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - quickActionsIntents = tokenActionsHandler, - currentState = currentState, - updateTokens = updateTokens, - ) - .convert(walletsWithCurrencies) - } - - private fun createAddToPortfolioBSConfig( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): TangemBottomSheetConfig { - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } - - val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() - - val alreadyAddedNetworks = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet] - ?.filter { !it.status.currency.isCustom } - ?.map { it.status.currency.network.backendId } - ?.toSet() - - return addToPortfolioBSContentUMFactory.create( - currentState = currentState().addToPortfolioBSConfig, - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - selectedWallet = selectedWallet, - alreadyAddedNetworks = alreadyAddedNetworks, - artworks = artworks, - ) - } - - private fun Map>.getAddButtonState( - availableNetworks: Set?, - ): AddButtonState { - if (availableNetworks == null) return AddButtonState.Loading - - val networkIds = availableNetworks.map { it.networkId } - - val isAllAvailableNetworksAdded = this - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available - } - - /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( - networks: Set, - ): Map> { - return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } - } - - /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( - networks: Set, - ): List { - val networkIds = networks.map(TokenMarketInfo.Network::networkId) - - return mapNotNull { currencyData -> - currencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) } - } - } -} \ 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 deleted file mode 100644 index 17acbaf94a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import arrow.core.getOrElse -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -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.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -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.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioHeader -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioListItem -import com.tangem.features.markets.portfolio.impl.ui.state.WalletHeader -import com.tangem.utils.extensions.isZero -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -@OptIn(ExperimentalCoroutinesApi::class) -@Suppress("LongParameterList") -internal class NewMarketsPortfolioDelegate @AssistedInject constructor( - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val allAccountSupplier: MultiAccountStatusListSupplier, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - @Assisted private val scope: CoroutineScope, - @Assisted private val token: TokenMarketParams, - @Assisted private val tokenActionsHandler: TokenActionsHandler, - @Assisted private val buttonState: Flow, - @Assisted private val onAddClick: () -> Unit, -) { - - private val currencyRawId: CryptoCurrency.RawID = token.id - private var expandedHolder: MutableStateFlow>>? = null - - private val settingsFlow: Flow = combine( - flow = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - flow3 = isAccountsModeEnabledUseCase(), - transform = ::SettingsBox, - ).shareIn( - replay = 1, - started = SharingStarted.Eagerly, - scope = scope, - ).distinctUntilChanged() - - private val availableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - fun setTokenNetworks(networks: List) { - availableNetworks.tryEmit(networks) - } - - fun combineData(): Flow { - return availableNetworks.transformLatest { availableNetworks -> - when { - availableNetworks.isEmpty() -> emit(MyPortfolioUM.Unavailable) - else -> emitAll(onAvailableNetworksFlow().distinctUntilChanged()) - } - }.distinctUntilChanged() - } - - private fun onAvailableNetworksFlow(): Flow = - portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency -> - when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) { - false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged()) - true -> when (portfolioWithCurrency.hasMultiWallets) { - true -> emitAll(addFirstTokenFlow()) - false -> emit(MyPortfolioUM.UnavailableForWallet) - } - } - } - - private fun addFirstTokenFlow(): Flow = buttonState.map { state -> - when (state) { - AddButtonState.Loading -> MyPortfolioUM.Loading - AddButtonState.Available -> MyPortfolioUM.AddFirstToken( - onAddClick = onAddClick, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - ) - AddButtonState.Unavailable -> MyPortfolioUM.Unavailable - } - } - - private fun contentFlow(portfolio: PortfoliosWithThisCurrency): Flow { - fun Portfolio.actionsFoAccountCurrencies(): List>> = - accountsWithAdded.map { account -> - fun CryptoCurrencyStatus.actionsFlow(): Flow> = flow { - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(this@actionsFlow.currency) - .getOrElse { YieldSupplyAvailability.Unavailable } - emitAll( - getCryptoCurrencyActionsUseCase( - accountId = account.accountStatus.account.accountId, - currency = this@actionsFlow.currency, - yieldSupplyAvailability = yieldSupplyAvailability, - ).map { actionsState -> actionsState.cryptoCurrencyStatus.currency to actionsState }, - ) - } - account.addedCurrency.map { it.actionsFlow() } - }.flatten() - - val allAddedTokenActions = - portfolio.portfolios.map { portfolioItem -> portfolioItem.actionsFoAccountCurrencies() }.flatten() - - return combine( - flow = combine(allAddedTokenActions) { it.toMap() }.distinctUntilChanged(), - flow2 = buttonState.distinctUntilChanged(), - flow3 = getExpandedHolder(portfolio), - flow4 = settingsFlow.distinctUntilChanged(), - transform = { actions, addButtonState, expanded, settings -> - buildContentState( - portfolio = portfolio, - allActions = actions, - addButtonState = addButtonState, - expanded = expanded, - settings = settings, - ) - }, - ) - } - - private fun getExpandedHolder( - portfolio: PortfoliosWithThisCurrency, - ): StateFlow>> { - val expandedHolder = this.expandedHolder - if (expandedHolder != null) return expandedHolder - val allAddedCurrency = portfolio.flattenAddedCurrency - val shouldForceExpand = allAddedCurrency.size == 1 && - allAddedCurrency.first().value.amount?.isZero() == true - - val initValue = when { - shouldForceExpand -> { - val currency = allAddedCurrency.first() - // find userWallet than have this single added token - portfolio.portfolios - .find { it.accountsWithAdded.any { account -> account.addedCurrency.isNotEmpty() } } - ?.userWallet - ?.let { setOf(it.walletId to currency.currency.id) } - .orEmpty() - } - else -> emptySet() - } - return MutableStateFlow(initValue) - .also { this.expandedHolder = it } - } - - private fun portfolioWithThisCurrencyFLow(): Flow = - allAccountSupplier().map { list -> list.map { it.addedAccountsFlow() } }.flatMapLatest { flows -> - combine(flows) { portfolios -> - PortfoliosWithThisCurrency( - currencyRawId = currencyRawId, - portfolios = portfolios.toList(), - ) - } - }.distinctUntilChanged() - - private fun AccountStatusList.addedAccountsFlow(): Flow = - getUserWalletUseCase.invokeFlow(this.userWalletId).mapNotNull { it.getOrNull() }.map { wallet -> - Portfolio( - userWallet = wallet, - accountStatusList = this, - accountsWithAdded = this.filterByRawID(), - ) - }.distinctUntilChanged() - - private fun AccountStatusList.filterByRawID(): List { - fun AccountStatus.filterByRawID(): List = when (this) { - is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() - .filter { status -> - val currencyId = status.currency.id.rawCurrencyId ?: return@filter false - getTokenIdIfL2Network(currencyId.value) == currencyRawId.value - } - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return accountStatuses.map { accountStatus -> - AccountWithAdded( - accountStatus = accountStatus, - addedCurrency = accountStatus.filterByRawID(), - ) - } - } - - private fun buildContentState( - portfolio: PortfoliosWithThisCurrency, - allActions: Map, - addButtonState: AddButtonState, - expanded: Set>, - settings: SettingsBox, - ): MyPortfolioUM.Content { - val appCurrency = settings.appCurrency - val isBalanceHidden = settings.isBalanceHidden - val isAccountMode = settings.isAccountMode - val uiItems: MutableList = mutableListOf() - - fun toggleQuickActions(key: Pair) = expandedHolder?.update { expanded -> - val isExpand = expanded.contains(key) - if (isExpand) expanded.minus(key) else expanded.plus(key) - } - - val tokenUMConverter = PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { }, - tokenActionsHandler = tokenActionsHandler, - ) - - portfolio.portfolios.forEach { portfolioItem -> - if (portfolioItem.flattenAddedCurrency.isEmpty()) return@forEach - val userWallet = portfolioItem.userWallet - if (isAccountMode) { - uiItems.add(portfolioItem.userWallet.toWalletHeader()) - } else { - uiItems.add(portfolioItem.userWallet.toWalletPortfolioHeader()) - } - - portfolioItem.accountsWithAdded.forEach { accountWithAdded -> - if (accountWithAdded.addedCurrency.isEmpty()) return@forEach - if (isAccountMode) { - val account = accountWithAdded.accountStatus.account - uiItems.add(account.toAccountPortfolioHeader()) - } - - accountWithAdded.addedCurrency.forEach { currencyStatus -> - val actions = allActions[currencyStatus.currency]?.states.orEmpty() - val value = PortfolioData.CryptoCurrencyData( - userWallet = userWallet, - status = currencyStatus, - actions = actions, - ) - val expandedKey = portfolioItem.userWallet.walletId to currencyStatus.currency.id - val isExpand = expanded.contains(expandedKey) - - val tokenItem = tokenUMConverter.convertV2( - onTokenItemClick = { wallet, status -> - toggleQuickActions(wallet.walletId to status.currency.id) - }, - value = value, - isQuickActionsShown = isExpand, - ) - uiItems.add(tokenItem) - } - } - } - - return MyPortfolioUM.Content( - items = uiItems.toImmutableList(), - buttonState = addButtonState, - onAddClick = onAddClick, - ) - } - - private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.accountId.value, - state = AccountTitleUM.Account( - prefixText = TextReference.EMPTY, - name = this.accountName.toUM().value, - icon = when (this) { - is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon) - is Account.Payment -> TODO("[REDACTED_JIRA]") - }, - ), - ) - - private fun UserWallet.toWalletPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.walletId.stringValue, - state = AccountTitleUM.Text( - title = stringReference(this.name), - ), - ) - - private fun UserWallet.toWalletHeader(): WalletHeader = WalletHeader( - id = this.walletId.stringValue, - name = stringReference(this.name), - ) - - @Suppress("LongParameterList") - @AssistedFactory - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - tokenActionsHandler: TokenActionsHandler, - buttonState: Flow, - onAddClick: () -> Unit, - ): NewMarketsPortfolioDelegate - } -} - -private data class PortfoliosWithThisCurrency( - val currencyRawId: CryptoCurrency.RawID, - val portfolios: List, -) { - - val hasMultiWallets: Boolean = portfolios.any { it.userWallet.isMultiCurrency } - - val flattenAddedCurrency: List = - portfolios.map { portfolio -> portfolio.flattenAddedCurrency }.flatten() -} - -private data class Portfolio( - val userWallet: UserWallet, - val accountStatusList: AccountStatusList, - val accountsWithAdded: List, -) { - val flattenAddedCurrency: List = - accountsWithAdded.map { it.addedCurrency }.flatten() -} - -private data class AccountWithAdded( - val addedCurrency: List, - val accountStatus: AccountStatus, -) - -private data class SettingsBox( - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val isAccountMode: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt deleted file mode 100644 index acf1b7934c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -/** - * Model for portfolio bottom sheet visibility - * - * @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet - * @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioBSVisibilityModel( - val isAddToPortfolioBSVisible: Boolean = false, - val isWalletSelectorBSVisible: Boolean = false, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt deleted file mode 100644 index 17b0a6cdf5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] - * -[REDACTED_AUTHOR] - */ -internal class PortfolioTokenUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, - private val tokenActionsHandler: TokenActionsHandler, -) : Converter { - - fun convertV2( - value: PortfolioData.CryptoCurrencyData, - isQuickActionsShown: Boolean, - onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit, - ): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - onItemClick = { _, status -> onTokenItemClick(value.userWallet, status) }, - ) - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = isQuickActionsShown, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) }, - subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name)) - }, - onItemClick = { _, status -> onTokenItemClick(status) }, - ) - - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = false, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - companion object { - fun quickActions( - cryptoData: PortfolioData.CryptoCurrencyData, - tokenActionsHandler: TokenActionsHandler, - ): PortfolioTokenUM.QuickActions { - return PortfolioTokenUM.QuickActions( - actions = toQuickActions(cryptoData.actions), - onQuickActionClick = { quickActionUM -> - when (quickActionUM) { - QuickActionUM.Buy -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Buy, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.Exchange -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Exchange, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Receive -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Receive, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Stake -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Stake, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.YieldMode -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.YieldMode, - cryptoCurrencyData = cryptoData, - ) - } - }, - onQuickActionLongClick = { quickAction -> - if (quickAction == QuickActionUM.Receive) { - tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.CopyAddress, - cryptoCurrencyData = cryptoData, - ) - } - }, - ) - } - - fun toQuickActions(actions: List) = buildList { - actions.forEach { action -> - if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { - when (action) { - is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy - is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange( - shouldShowBadge = action.showBadge, - ) - is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy) - else -> null - }?.let(::add) - } - } - }.toImmutableList() - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt deleted file mode 100644 index 855e596731..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Portfolio UI data. Combined data from all UI flows that required to setup portfolio - * - * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @property selectedWalletId selected wallet id - * @property addToPortfolioData add to portfolio data - * @property shouldRequireColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioUIData( - val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - val selectedWalletId: UserWalletId?, - val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - val shouldRequireColdWalletInteraction: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt deleted file mode 100644 index 8599ad97d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [TokenMarketParams] to [SelectNetworkUM] - * - * @property networksWithToggle map of networks with toggles - * @property alreadyAddedNetworks already added networks - * @property onNetworkSwitchClick callback is called when network switch is clicked - * -[REDACTED_AUTHOR] - */ -internal class SelectNetworkUMConverter( - private val networksWithToggle: Map, - private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketParams): SelectNetworkUM { - return SelectNetworkUM( - tokenId = value.id.value, - iconUrl = value.imageUrl, - tokenName = value.name, - tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter(alreadyAddedNetworks) - .convertList(networksWithToggle.toList()) - .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt deleted file mode 100644 index 66ff85116c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.routing.AppRoute -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList - -@Suppress("LongParameterList") -internal class TokenActionsHandler @AssistedInject constructor( - private val router: Router, - private val clipboardManager: ClipboardManager, - private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, - @Assisted private val currentAppCurrency: Provider, - @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, - private val isDemoCardUseCase: IsDemoCardUseCase, - private val messageSender: UiMessageSender, -) { - - private val disabledActionsInDemoMode = buildSet { - add(TokenActionsBSContentUM.Action.Sell) - } - - fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - onHandleQuickAction( - HandledQuickAction( - action = action, - cryptoCurrencyData = cryptoCurrencyData, - ), - ) - val userWallet = cryptoCurrencyData.userWallet - if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return - - when (action) { - TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Receive -> Unit - TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData) - } - } - - private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean { - val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId) - val shouldShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action) - - if (shouldShowDemoWarning) { - showDemoModeWarning() - } - - return shouldShowDemoWarning - } - - private fun showDemoModeWarning() { - val message = DialogMessage( - message = resourceReference(R.string.alert_demo_feature_disabled), - ) - messageSender.send(message) - } - - private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val cryptoCurrencyStatus = cryptoCurrencyData.status - val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return - val addresses = networkAddress.availableAddresses - .mapToAddressModels(cryptoCurrencyStatus.currency) - .toImmutableList() - val defaultAddress = addresses.firstOrNull()?.value ?: return - - clipboardManager.setText(text = defaultAddress, isSensitive = true) - uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) - } - - private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Onramp( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - source = OnrampSource.MARKETS, - ), - ) - } - - private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) - } - - private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Swap( - currencyFrom = cryptoCurrencyData.status.currency, - userWalletId = cryptoCurrencyData.userWallet.walletId, - isInitialReverseOrder = true, - screenSource = AnalyticsParam.ScreensSources.Markets.value, - ), - ) - } - - private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val route = AppRoute.SendEntryPoint( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - ) - router.push(route) - } - - private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } - ?.let { it as TokenActionsState.ActionState.Stake } - ?.option ?: return - - router.push( - AppRoute.Staking( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - integrationId = option.integrationId, - ), - ) - } - - private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() - .firstOrNull()?.apy ?: return - - router.push( - AppRoute.YieldSupplyEntry( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - apy = yieldSupplyApy, - ), - ) - } - - @AssistedFactory - interface Factory { - fun create( - currentAppCurrency: Provider, - onHandleQuickAction: (HandledQuickAction) -> Unit, - ): TokenActionsHandler - } - - data class HandledQuickAction( - val action: TokenActionsBSContentUM.Action, - val cryptoCurrencyData: PortfolioData.CryptoCurrencyData, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt deleted file mode 100644 index 16f7f6554a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class TokensPortfolioUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val addButtonState: AddButtonState, - private val bsConfig: TangemBottomSheetConfig, - private val onAddClick: () -> Unit, - private val quickActionsIntents: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { - - override fun convert(value: Map>): MyPortfolioUM.Tokens { - val currentTokensState = currentState() as? MyPortfolioUM.Tokens - - return MyPortfolioUM.Tokens( - tokens = value - .flatMap { entry -> entry.value } - .map { cryptoData -> - PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { toggleQuickActions(cryptoData) }, - tokenActionsHandler = quickActionsIntents, - ).convert(value = cryptoData) to cryptoData - } - .setQuickActionsVisibility(currentState = currentTokensState) - .toImmutableList(), - buttonState = addButtonState, - addToPortfolioBSConfig = bsConfig, - onAddClick = onAddClick, - ) - } - - private fun List>.setQuickActionsVisibility( - currentState: MyPortfolioUM.Tokens?, - ): List { - return when { - // if there is only one token and it has empty balance, show quick actions for it - currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = true) - } - } - // if there is no previous state, hide quick actions for all tokens - currentState == null -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = false) - } - } - else -> { - val previousList = currentState.tokens - - // otherwise, keep previous state - this.map { (token, _) -> - token.copy( - isQuickActionsShown = previousList - .firstOrNull { it.matchWith(token) } - ?.isQuickActionsShown == true, - ) - } - } - } - } - - private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return cryptoData.status.value.amount?.isZero() == true - } - - private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { - updateTokens { tokenList -> - tokenList.map { token -> - token.copy( - isQuickActionsShown = if (token.matchWith(cryptoData)) { - !token.isQuickActionsShown - } else { - false - }, - ) - }.toImmutableList() - } - } - - private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { - return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id - } - - private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return this.walletId == cryptoData.userWallet.walletId && - this.tokenItemState.id == cryptoData.status.currency.id.value - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt deleted file mode 100644 index f2d2d22dd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.rows.ArrowRow -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -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.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.coroutines.delay - -@Composable -internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - titleText = resourceReference(R.string.common_add_to_portfolio), - ) { contentState -> - Content( - modifier = Modifier.fillMaxWidth(), - state = contentState, - ) - - WalletSelectorBottomSheet(contentState.walletSelectorConfig) - } -} - -@Composable -private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - var continueButtonAreaHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val scrollState = rememberScrollState() - - Box(modifier = modifier) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.isWalletBlockVisible) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - SpacerH12() - } - - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - SpacerH12() - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) - SpacerH12() - } - - // Scroll to the bottom when the notification appears and the scroll is at the bottom - LaunchedEffect(Unit) { - if (scrollState.canScrollForward.not()) { - delay(timeMillis = 500) - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - - SpacerH(with(density) { continueButtonAreaHeight.toDp() }) - } - - AnimatedVisibility( - visible = scrollState.canScrollForward, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter), - ) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - - ContinueButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - continueButtonAreaHeight = it.size.height - }, - enabled = state.isContinueButtonEnabled, - isTangemIconVisible = state.isScanCardNotificationVisible, - onClick = state.onContinueButtonClick, - ) - } -} - -@Composable -private fun ContinueButton( - enabled: Boolean, - isTangemIconVisible: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TangemButton( - enabled = enabled, - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ) - .navigationBarsPadding() - .fillMaxWidth(), - text = stringResourceSafe(R.string.common_continue), - icon = if (enabled && isTangemIconVisible) { - TangemButtonIconPosition.End(R.drawable.ic_tangem_24) - } else { - TangemButtonIconPosition.None - }, - showProgress = false, - size = TangemButtonSize.Default, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - onClick = onClick, - animateContentChange = true, - ) -} - -@Suppress("LongMethod") -@Composable -private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(R.string.markets_select_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing14), - verticalAlignment = Alignment.CenterVertically, - ) { - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - SpacerW12() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = false) - .alignByBaseline(), - text = state.tokenName, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW6() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .alignByBaseline(), - text = state.tokenCurrencySymbol, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Visible, - maxLines = 1, - ) - } - - state.networks.fastForEachIndexed { index, network -> - ArrowRow( - isLastItem = index == state.networks.lastIndex, - content = { - BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - model = network, - action = { - TangemSwitch( - checked = network.isSelected, - checkedColor = if (network.isEnabled) { - TangemTheme.colors.control.checked - } else { - TangemTheme.colors.icon.inactive - }, - onCheckedChange = { checked -> - if (checked) { - hapticManager.perform(TangemHapticEffect.View.ToggleOn) - } else { - hapticManager.perform(TangemHapticEffect.View.ToggleOff) - } - - state.onNetworkSwitchClick(network, checked) - }, - enabled = network.isEnabled, - ) - }, - ) - }, - ) - } - } - } -} - -@Composable -private fun ScanWalletWarning(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background( - color = TangemTheme.colors.button.disabled, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.markets_generate_addresses_notification), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - content = content, - onDismissRequest = {}, - ), - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContent( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContentRtl( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview(rtl = true) { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -// For on device testing -@Composable -@Preview -private fun PreviewContentTestOnDevice( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview( - alwaysShowBottomSheets = false, - ) { - var isShow by remember { mutableStateOf(false) } - - var contentState by remember { - mutableStateOf(content) - } - - LaunchedEffect(Unit) { - contentState = content.copy( - onContinueButtonClick = { - contentState = contentState.copy( - isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, - ) - }, - isContinueButtonEnabled = true, - selectedWallet = content.selectedWallet.copy( - onClick = { - contentState = contentState.copy( - isContinueButtonEnabled = !contentState.isContinueButtonEnabled, - ) - }, - ), - ) - } - - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = isShow, - content = contentState, - onDismissRequest = { isShow = false }, - ), - ) - - Button( - onClick = { isShow = !isShow }, - ) { - Text(text = "Toggle") - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt deleted file mode 100644 index 1ea37db49c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ /dev/null @@ -1,339 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.account.AccountTitle -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SmallButtonShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -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.extensions.resolveReference -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.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState - -@Composable -internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { - if (state is MyPortfolioUM.Content) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing32, - ) - PortfolioList(state, contentModifier) - return - } - InformationBlock( - modifier = modifier, - contentHorizontalPadding = TangemTheme.dimens.spacing0, - title = { Title() }, - action = { - if (state !is MyPortfolioUM.Tokens) return@InformationBlock - - AddButton(state = state.buttonState, onClick = state.onAddClick) - }, - ) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ) - - when (state) { - is MyPortfolioUM.Tokens -> TokenList(state = state) - is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) - MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) - MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier) - MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier) - is MyPortfolioUM.Content -> PortfolioList(state = state) - } - } - - val bsConfig = state.addToPortfolioBSConfig - if (bsConfig != null) { - AddToPortfolioBottomSheet(config = bsConfig) - } -} - -@Composable -private fun Title() { - Text( - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddButton(state: AddButtonState, onClick: () -> Unit) { - when (state) { - AddButtonState.Loading -> { - Box { - SmallButtonShimmer( - modifier = Modifier.width(width = TangemTheme.dimens.size63), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - withIcon = true, - ) - - Box( - Modifier - .matchParentSize() - .background(TangemTheme.colors.background.action), - ) - - RectangleShimmer( - modifier = Modifier - .align(Alignment.Center) - .size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), - radius = TangemTheme.dimens.radius3, - ) - } - } - AddButtonState.Available, - AddButtonState.Unavailable, - -> { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.markets_add_token), - icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), - onClick = onClick, - isEnabled = state == AddButtonState.Available, - ), - ) - } - } -} - -@Composable -private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { - Column(modifier) { - state.tokens.fastForEachIndexed { index, token -> - key(token.tokenItemState.id) { - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = token, - lastInList = index == state.tokens.size - 1, - ) - } - } - } -} - -@Composable -private fun PortfolioList(state: MyPortfolioUM.Content, modifier: Modifier = Modifier) { - Column(modifier) { - key("PortfolioListHeader") { - Row( - modifier = Modifier.padding(horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - AddButton(state = state.buttonState, onClick = state.onAddClick) - } - } - - state.items.fastForEachIndexed { index, item -> - val previousItem = state.items.getOrNull(index.dec()) - val nextItem = state.items.getOrNull(index.inc()) - val itemModifier = Modifier - .fillMaxWidth() - .getOffsetModifier(item, previousItem) - .getBackgroundModifier(item, previousItem, nextItem) - - key(item.id) { - PortfolioItem( - item = item, - modifier = itemModifier, - lastInList = index == state.items.size - 1, - ) - } - } - } -} - -@Composable -private fun Modifier.getBackgroundModifier( - item: PortfolioListItem, - previousItem: PortfolioListItem?, - nextItem: PortfolioListItem?, -): Modifier { - val color = TangemTheme.colors.background.action - val radius = 14.dp - val topRound = RoundedCornerShape(topStart = radius, topEnd = radius) - val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - val allRound = RoundedCornerShape(size = radius) - val backgroundModifier = when (item) { - is WalletHeader -> this - is PortfolioHeader -> this - .clip(topRound) - .background(color = color) - is PortfolioTokenUM -> when { - previousItem is PortfolioHeader && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - previousItem is WalletHeader && nextItem !is PortfolioTokenUM -> this - .clip(allRound) - .background(color = color) - previousItem is PortfolioTokenUM && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - else -> this.background(color = color) - } - } - return backgroundModifier -} - -private fun Modifier.getOffsetModifier(item: PortfolioListItem, previousItem: PortfolioListItem?): Modifier = when { - item is WalletHeader -> this.padding(top = 20.dp, start = 4.dp, end = 4.dp) - item is PortfolioHeader && previousItem is PortfolioTokenUM -> this.padding(top = 12.dp) - item is PortfolioHeader && previousItem == null -> this.padding(top = 20.dp) - previousItem is WalletHeader -> this.padding(top = 12.dp) - previousItem is PortfolioTokenUM -> this - else -> this -} - -@Composable -private fun PortfolioItem(item: PortfolioListItem, lastInList: Boolean, modifier: Modifier = Modifier) { - when (item) { - is PortfolioHeader -> AccountTitle( - modifier = modifier.padding( - start = 12.dp, - top = 12.dp, - bottom = 8.dp, - ), - accountTitleUM = item.state, - textStyle = TangemTheme.typography.caption1, - textColor = TangemTheme.colors.text.primary1, - ) - is PortfolioTokenUM -> PortfolioItem( - state = item, - modifier = modifier, - lastInList = lastInList, - ) - is WalletHeader -> Text( - modifier = modifier, - text = item.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - } -} - -@Composable -fun UnavailableAsset(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_description, - modifier = modifier, - ) -} - -@Composable -fun UnavailableAssetForWallet(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description, - modifier = modifier, - ) -} - -@Composable -private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResourceSafe(textId), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = stringResourceSafe(R.string.markets_add_to_my_portfolio_description), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_add_to_portfolio), - onClick = state.onAddClick, - ) - } -} - -@Composable -private fun LoadingPlaceholder(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.7f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary), - ) { - MyPortfolio(state) - } - } -} - -@Preview -@Composable -private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview(rtl = true) { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing8), - ) { - MyPortfolio(state) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt deleted file mode 100644 index d7800514fc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -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.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.icons.IconTint -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -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.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns.DASH_SIGN -import kotlinx.collections.immutable.persistentListOf -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState - -@Composable -internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - Column(modifier) { - val hapticManager = LocalHapticManager.current - val tokenItemState = remember(state.tokenItemState) { - when (state.tokenItemState) { - is TokenItemState.Content -> state.tokenItemState.copy( - onItemClick = { cryptoCurrency -> - val onClick = state.tokenItemState.onItemClick - if (onClick != null) { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - onClick.invoke(cryptoCurrency) - } - }, - ) - else -> state.tokenItemState - } - } - TokenItem( - state = tokenItemState, - isBalanceHidden = state.isBalanceHidden, - itemPaddingValues = PaddingValues( - start = TangemTheme.dimens.spacing10, - end = TangemTheme.dimens.spacing12, - ), - ) - - PortfolioQuickActions( - modifier = Modifier - .padding( - bottom = if (lastInList) { - TangemTheme.dimens.spacing12 - } else { - TangemTheme.dimens.spacing24 - }, - ), - actions = state.quickActions.actions, - isVisible = state.isQuickActionsShown, - onActionClick = state.quickActions.onQuickActionClick, - onActionLongClick = state.quickActions.onQuickActionLongClick, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { - TangemThemePreview { - var areQuickActionsShown by remember { mutableStateOf(value = false) } - - val onItemClick = { - areQuickActionsShown = areQuickActionsShown.not() - } - - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = tokenUM.copy( - tokenItemState = when (tokenUM.tokenItemState) { - is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - else -> tokenUM.tokenItemState - }, - isQuickActionsShown = areQuickActionsShown, - ), - lastInList = true, - ) - } -} - -private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - tokenUM.copy( - tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( - fiatAmountState = contentFiatAmount.copy( - icons = persistentListOf( - TokenFiatAmountState.Content.IconUM( - iconRes = R.drawable.ic_staking_24, - tint = IconTint.Accent, - ), - ), - ), - ), - ), - tokenUM.copy( - tokenItemState = tokenUM.tokenItemState.copy( - fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), - subtitle2State = (tokenUM.tokenItemState.subtitle2State as? TokenItemState.Subtitle2State.TextContent - ?: error("subtitle2State must be TextContent for preview")) - .copy(text = DASH_SIGN), - ), - ), - tokenUM.copy(isBalanceHidden = true), - tokenUM.copy( - tokenItemState = TokenItemState.Unreachable( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemClick = {}, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.NoAddress( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.Loading( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, - subtitleState = tokenUM.tokenItemState.subtitleState, - ), - ), - ), -) { - - companion object { - val tokenUM = PreviewMyPortfolioUMProvider().sampleToken - val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as? TokenFiatAmountState.Content - ?: error("fiatAmountState must be Content for preview") - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt deleted file mode 100644 index 22a377bfd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -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.* -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.* -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.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.extensions.resolveReference -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.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PortfolioQuickActions( - actions: ImmutableList, - isVisible: Boolean, - onActionClick: (QuickActionUM) -> Unit, - onActionLongClick: (QuickActionUM) -> Unit, - modifier: Modifier = Modifier, -) { - if (actions.isEmpty()) return - - AnimatedVisibility( - visible = isVisible, - enter = expandVertically(expandFrom = Alignment.Top), - exit = shrinkVertically(shrinkTowards = Alignment.Top), - modifier = modifier, - ) { - Column(modifier = Modifier) { - actions.fastForEach { action -> - LineSeparator() - QuickActionItem( - state = action, - onClick = { onActionClick(action) }, - onLongClick = { onActionLongClick(action) }.takeIf { action.isLongClickAvailable }, - ) - } - } - } -} - -@Composable -private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { - val lineColor = TangemTheme.colors.stroke.primary - val strokeWidth = TangemTheme.dimens.size1 - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val startPadding = TangemTheme.dimens.spacing30 - - val height = TangemTheme.dimens.size16 - - Canvas( - modifier = modifier - .animateEnterExit( - enter = expandVertically( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - ), - expandFrom = Alignment.Top, - ) + fadeIn(), - exit = shrinkVertically( - spring( - stiffness = Spring.StiffnessLow, - ), - shrinkTowards = Alignment.Top, - ) + fadeOut(), - ) - .fillMaxWidth() - .height(height), - ) { - val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() - - drawLine( - color = lineColor, - start = Offset(x, 0f), - end = Offset(x, size.height), - strokeWidth = strokeWidth.toPx(), - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun AnimatedVisibilityScope.QuickActionItem( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit)?, - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) { - { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - } else { - null - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), - ) { - QuickActionIcon(state) - Column( - modifier = Modifier - .animateEnterExit( - enter = fadeIn(), - exit = fadeOut(), - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { - val containerColor = TangemTheme.colors.background.action - Box( - Modifier - .animateEnterExit( - enter = scaleIn(), - exit = scaleOut(), - ) - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ) - .size(TangemTheme.dimens.size32) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.button.primary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - var isVisible by remember { mutableStateOf(true) } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(680.dp), - ) { - Button( - onClick = { isVisible = !isVisible }, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) { - Text(text = "Toggle") - } - SpacerH4() - Box( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - ) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = isVisible, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewRtl() { - TangemThemePreview(rtl = true) { - Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = true, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt deleted file mode 100644 index 8cb81049e5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -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.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - title = { content -> - TangemBottomSheetTitle(content.title) - }, - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: TokenActionsBSContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - content.actions.forEachIndexed { index, action -> - Box( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.actions.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - ) { - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconRes, - redesign = true, - onItemsClick = { content.onActionClick(action) }, - ) - } - } - } -} - -@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)) { - TokenActionsBottomSheet( - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TokenActionsBSContentUM( - title = "Wallet 1", - actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), - onActionClick = {}, - ), - ), - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt deleted file mode 100644 index 23e63c41f4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -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.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - title = { content -> - TangemTopAppBar( - title = resourceReference(R.string.common_choose_wallet), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(content.onBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - }, - ) { content -> - Content( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - state = content, - ) - } -} - -@Composable -private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Column( - modifier = modifier - .verticalScroll(rememberScrollState()), - ) { - BlockCard( - modifier = Modifier.fillMaxSize(), - colors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( - modifier = Modifier.fillMaxWidth(), - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - state = state, - ) - } - } - } - SpacerH(bottomBarHeight) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - WalletSelectorBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewContent() { - TangemThemePreview { - Content( - state = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt deleted file mode 100644 index bdc1b092e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.collections.immutable.persistentListOf - -internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = "1", - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ) - - val userWallet = UserWalletItemUM( - id = "1", - name = stringReference("Wallet 1"), - information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), - balance = UserWalletItemUM.Balance.Loading, - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ), - blockchainRow, - blockchainRow, - ), - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = true, - isContinueButtonEnabled = true, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium Etherium Etherium Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ).copy(name = "Etherium Etherium Etherium Etherium"), - *Array(25) { blockchainRow }, - ), - - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = false, - isContinueButtonEnabled = false, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt deleted file mode 100644 index e6deaad138..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.account.AccountIconPreviewData -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { - - val sampleToken - get() = PortfolioTokenUM( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = "XRP Ledger token"), - ), - onItemClick = {}, - onItemLongClick = {}, - ), - isQuickActionsShown = false, - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - isBalanceHidden = false, - walletId = UserWalletId(""), - ) - - val walletHeader - get() = WalletHeader( - id = UUID.randomUUID().toString(), - name = stringReference("Wallet 1"), - ) - - val walletPortfolioHeader - get() = PortfolioHeader( - state = AccountTitleUM.Text(title = stringReference("Wallet 1")), - id = UUID.randomUUID().toString(), - ) - - val accountHeader - get() = PortfolioHeader( - state = AccountTitleUM.Account( - icon = AccountIconPreviewData.randomAccountIcon(), - name = stringReference("Main Account"), - prefixText = TextReference.EMPTY, - ), - id = UUID.randomUUID().toString(), - ) - val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22, - isGrayscale = false, - shouldShowCustomBadge = false, - ) - val accountToken - get() = sampleToken.copy( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = coinIconState, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - fiatAmountState = FiatAmountState.Content(text = "321 $"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")), - onItemClick = {}, - onItemLongClick = {}, - ), - ) - - override val values: Sequence - get() = sequenceOf( - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletPortfolioHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken.copy(isQuickActionsShown = true), - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Loading, - MyPortfolioUM.Unavailable, - MyPortfolioUM.UnavailableForWallet, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt deleted file mode 100644 index ff5c1567e6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent - -internal data class AddToPortfolioBSContentUM( - val selectedWallet: UserWalletItemUM, - val selectNetworkUM: SelectNetworkUM, - val isWalletBlockVisible: Boolean, - val isScanCardNotificationVisible: Boolean, - val isContinueButtonEnabled: Boolean, - val onContinueButtonClick: () -> Unit, - val walletSelectorConfig: TangemBottomSheetConfig, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt deleted file mode 100644 index 0d1cd7c700..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed class MyPortfolioUM { - - abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? - - data class Tokens( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val tokens: ImmutableList, - val buttonState: AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - enum class AddButtonState { - Loading, - Available, - Unavailable, - } - } - - data class Content( - val items: ImmutableList, - val buttonState: Tokens.AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty - } - - data class AddFirstToken( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() - - data object Loading : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object Unavailable : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object UnavailableForWallet : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt deleted file mode 100644 index b6dd772abc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed interface PortfolioListItem { - val id: String -} - -internal data class WalletHeader( - override val id: String, - val name: TextReference, -) : PortfolioListItem - -internal data class PortfolioHeader( - override val id: String, - val state: AccountTitleUM, -) : PortfolioListItem - -internal data class PortfolioTokenUM( - val tokenItemState: TokenItemState, - val walletId: UserWalletId, - val isBalanceHidden: Boolean, - val isQuickActionsShown: Boolean, - val quickActions: QuickActions, -) : PortfolioListItem { - override val id: String = tokenItemState.id - - data class QuickActions( - val actions: ImmutableList, - val onQuickActionClick: (QuickActionUM) -> Unit, - val onQuickActionLongClick: (QuickActionUM) -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt deleted file mode 100644 index 040f3d243e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -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.wrappedList -import com.tangem.features.markets.impl.R - -@Immutable -internal sealed class QuickActionUM( - val title: TextReference, - val description: TextReference, - @DrawableRes val icon: Int, - val isLongClickAvailable: Boolean = false, -) { - data object Buy : QuickActionUM( - title = resourceReference(R.string.common_buy), - description = resourceReference(R.string.buy_token_description), - icon = R.drawable.ic_plus_24, - ) - - data class Exchange( - val shouldShowBadge: Boolean, - ) : QuickActionUM( - title = resourceReference(R.string.common_exchange), - description = resourceReference(R.string.exсhange_token_description), - icon = R.drawable.ic_exchange_vertical_24, - ) - - data object Receive : QuickActionUM( - title = resourceReference(R.string.common_receive), - description = resourceReference(R.string.receive_token_description), - icon = R.drawable.ic_arrow_down_24, - isLongClickAvailable = true, - ) - - data object Stake : QuickActionUM( - title = resourceReference(R.string.common_stake), - description = resourceReference(R.string.stake_token_description), - icon = R.drawable.ic_staking_24, - ) - - data class YieldMode( - private val apy: String, - ) : QuickActionUM( - title = resourceReference(R.string.common_yield_mode), - description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), - icon = R.drawable.ic_analytics_up_mini_24, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt deleted file mode 100644 index 90830679ca..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -internal data class SelectNetworkUM( - val tokenId: String, - val iconUrl: String?, - val tokenName: String, - val tokenCurrencySymbol: String, - val networks: ImmutableList, - val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt deleted file mode 100644 index 20db6ec796..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList - -internal data class TokenActionsBSContentUM( - val title: String, - val actions: ImmutableList, - val onActionClick: (Action) -> Unit, -) : TangemBottomSheetConfigContent { - - @Immutable - enum class Action( - val text: TextReference, - @DrawableRes val iconRes: Int, - ) { - CopyAddress( - text = resourceReference(R.string.common_copy_address), - iconRes = R.drawable.ic_copy_24, - ), - Send( - text = resourceReference(R.string.common_send), - iconRes = R.drawable.ic_arrow_up_24, - ), - Receive( - text = resourceReference(R.string.common_receive), - iconRes = R.drawable.ic_arrow_down_24, - ), - Buy( - text = resourceReference(R.string.common_buy), - iconRes = R.drawable.ic_plus_24, - ), - Sell( - text = resourceReference(R.string.common_sell), - iconRes = R.drawable.ic_currency_24, - ), - Exchange( - text = resourceReference(R.string.common_exchange), - iconRes = R.drawable.ic_exchange_horizontal_24, - ), - Stake( - text = resourceReference(R.string.common_stake), - iconRes = R.drawable.ic_staking_24, - ), - YieldMode( - text = resourceReference(R.string.common_yield_mode), - iconRes = R.drawable.ic_analytics_up_mini_24, - ), - ; - - val order: Int = ordinal - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt deleted file mode 100644 index fddc12c25e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletSelectorBSContentUM( - val userWallets: ImmutableList, - val onBack: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt new file mode 100644 index 0000000000..2db32b29db --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.token.block.impl.model.formatter + +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.marketprice.PriceChangeType + +internal fun PriceChangeType.toChartType(): MarketChartLook.Type { + return when (this) { + PriceChangeType.UP -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 8f233b53ab..362075a2e2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -23,8 +23,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.model.formatter.toChartType import com.tangem.features.markets.impl.R +import com.tangem.features.markets.token.block.impl.model.formatter.toChartType import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import kotlinx.collections.immutable.toImmutableList import kotlin.random.Random diff --git a/features/nft/impl/detekt-baseline-debug.xml b/features/nft/impl/detekt-baseline-debug.xml index 97fee15b2b..cadeff498d 100644 --- a/features/nft/impl/detekt-baseline-debug.xml +++ b/features/nft/impl/detekt-baseline-debug.xml @@ -5,34 +5,23 @@ BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM$val showAllTraitsButton: Boolean BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.BlockItem$val showInfoButton: Boolean BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.Rarity.Content$val showDivider: Boolean - BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } - BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true BooleanPropertyNaming:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$val custom = derivationPath is Network.DerivationPath.Custom MultilineLambdaItParameter:ChangeCollectionExpandedStateTransformer.kt$ChangeCollectionExpandedStateTransformer${ val collectionId = collection.collectionIdProvider() if (it.id == collectionId && it is NFTCollectionUM) { if (!it.isExpanded) { onFirstExpanded() } it.copy(isExpanded = !it.isExpanded) } else { it } } MultilineLambdaItParameter:NFTCollectionsContent.kt${ key(it.id) { NFTCollectionWarning( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16), state = it, ) } } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ ChangeCollectionExpandedStateTransformer( collection = collection, collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = listOf(), isAccountMode = isAccountMode, walletNFTCollections = nftCollections.copy(collections = filteredNFTs), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -> params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = nftCollections.filter(query), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -> params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ it.copy( content = when (val content = it.content) { is NFTCollections.Content.Collections -> content.copy( collections = content.collections.orEmpty().filter { val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery }, ) is NFTCollections.Content.Error -> it.content }, ) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.BlockItem( title = stringReference(it.name), value = it.value, showInfoButton = false, ) } MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.Media.Content( url = it, ) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ ShowReceiveBottomSheetTransformer( network = network, networkAddress = value.address, onDismissBottomSheet = ::onReceiveBottomSheetDismiss, onCopyClick = { text -> onCopyClick(text, network) }, onShareClick = { text -> onShareClick(text, network) }, ).transform(it) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ UpdateDataStateTransformer( networks = filteredNetworks, onNetworkClick = ::onNetworkClick, ).transform(it) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ it.copy( bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), ) } MultilineLambdaItParameter:UpdateDataStateTransformer.kt$UpdateDataStateTransformer${ NFTCollectionUM( id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.rawId), name = it.name.orEmpty(), description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, wrappedList(it.count), ), logoUrl = it.logoUrl, assets = it.transformAssets(), onExpandClick = { onExpandCollectionClick(it) }, isExpanded = it.isExpanded(state), ) } - NoNameShadowing:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } NullableBooleanCheck:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$(state.content as? NFTCollectionsUM.Content) ?.collections ?.filterIsInstance<NFTCollectionUM>() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false NullableToStringCall:NFTCollectionsContent.kt$${item2?.id} - NullableToStringCall:NFTCollectionsModel.kt$NFTCollectionsModel$${network.derivationPath.value} PropertyUsedBeforeDeclaration:NFTDetailsModel.kt$NFTDetailsModel$_state ReusedModifierInstance:NFTCollectionsContent.kt$Box( modifier = modifier .fillMaxSize() .padding(bottom = bottomPadding), ) { Text( modifier = Modifier .align(Alignment.Center), text = stringResourceSafe(id = R.string.nft_empty_search), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) } ReusedModifierInstance:NFTCollectionsLoading.kt$Card( modifier = modifier .fillMaxWidth() .padding( top = TangemTheme.dimens.spacing16, ), shape = RoundedCornerShape(TangemTheme.dimens.radius16), colors = CardDefaults.cardColors( containerColor = TangemTheme.colors.background.primary, contentColor = TangemTheme.colors.text.primary1, disabledContainerColor = TangemTheme.colors.background.primary, disabledContentColor = TangemTheme.colors.text.primary1, ), ) { Column { repeat(SHIMMER_ITEMS_COUNT) { CollectionPlaceholder() } } } ReusedModifierInstance:NFTDetailsAsset.kt$Column( modifier = modifier .verticalScroll(scrollState) .padding( start = TangemTheme.dimens.spacing16, top = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, bottom = bottomPadding, ) .fillMaxSize(), ) { NFTDetailsLogo( state = state.media, modifier = Modifier .aspectRatio(1f), ) NFTDetailsInfoGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), state = state.topInfo, onReadMoreClick = onReadMoreClick, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.traits, title = resourceReference(R.string.nft_details_traits), action = if (state.showAllTraitsButton) { { NFTBlocksGroupAction( text = resourceReference(R.string.common_see_all), startIcon = { }, onClick = onSeeAllTraitsClick, ) } } else { null }, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.baseInfoItems, title = resourceReference(R.string.nft_details_base_information), action = { NFTBlocksGroupAction( text = resourceReference(R.string.common_explore), startIcon = { NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24) }, onClick = onExploreClick, ) }, ) } ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), ) { TextShimmer( modifier = Modifier .width(TangemTheme.dimens.size158), style = TangemTheme.typography.head, textSizeHeight = true, ) TextShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .width(TangemTheme.dimens.size90), style = TangemTheme.typography.caption2, textSizeHeight = true, ) } ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), verticalArrangement = Arrangement.SpaceAround, ) { Text( text = state.cryptoPrice.resolveReference(), style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) Text( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .flicker(state.isFlickering), text = state.fiatPrice.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - UnsafeCallOnNullableType:DefaultNFTComponent.kt$DefaultNFTComponent$portfolioFetcher!! - UseEmptyCounterpart:NFTCollectionsModel.kt$NFTCollectionsModel$listOf() UseEmptyCounterpart:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$listOf() UseOrEmpty:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$walletNFTCollections.collections.values.firstOrNull() ?: listOf() diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 20f1fc3414..2f6f77b6c7 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase @@ -33,7 +32,6 @@ internal class NFTCollectionsModel @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, private val refreshAllNFTUseCase: RefreshAllNFTUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -66,51 +64,21 @@ internal class NFTCollectionsModel @Inject constructor( } init { - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeToNFTCollectionsNew() - } else { - subscribeToNFTCollections() - } + subscribeToNFTCollections() } private fun subscribeToNFTCollections() { combine( flow = getNFTCollectionsUseCase(params.userWalletId), flow2 = searchManager.query.distinctUntilChanged(), - ) { nftCollections, query -> - _state.update { - UpdateDataStateTransformer( - nftCollections = nftCollections.filter(query), - onReceiveClick = { - params.onReceiveClick() - }, - onRetryClick = ::onRefresh, - onExpandCollectionClick = ::onExpandCollectionClick, - onRetryAssetsClick = ::onRetryAssetsClick, - onAssetClick = { asset, collection -> - params.onAssetClick(asset, collection) - }, - initialSearchBarFactory = ::getInitialSearchBar, - collectionIdProvider = collectionIdProvider, - ).transform(it) - } - } - .onStart { onRefresh() } - .launchIn(modelScope) - } - - private fun subscribeToNFTCollectionsNew() { - combine( - flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId), - flow2 = searchManager.query.distinctUntilChanged(), flow3 = isAccountsModeEnabledUseCase(), ) { nftCollections, query, isAccountMode -> val filteredNFTs = nftCollections.collections .mapValues { (_, nfts) -> nfts.filter(query) } - _state.update { + _state.update { stateUM -> UpdateDataStateTransformer( - nftCollections = listOf(), + nftCollections = emptyList(), isAccountMode = isAccountMode, walletNFTCollections = nftCollections.copy(collections = filteredNFTs), onReceiveClick = { @@ -124,22 +92,22 @@ internal class NFTCollectionsModel @Inject constructor( }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, - ).transform(it) + ).transform(stateUM) } } .onStart { onRefresh() } .launchIn(modelScope) } - private fun List.filter(query: String): List = map { - it.copy( - content = when (val content = it.content) { + private fun List.filter(query: String): List = map { collections -> + collections.copy( + content = when (val content = collections.content) { is NFTCollections.Content.Collections -> content.copy( - collections = content.collections.orEmpty().filter { - val assetsFulfillQuery = if (query.isEmpty()) { + collections = content.collections.orEmpty().filter { collection: NFTCollection -> + val isAssetsFulfillQuery = if (query.isEmpty()) { true } else { - when (val assets = it.assets) { + when (val assets = collection.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, @@ -152,13 +120,13 @@ internal class NFTCollectionsModel @Inject constructor( } } - val collectionFulfillQuery = - query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true + val isCollectionFulfillQuery = + query.isEmpty() || collection.name?.lowercase()?.contains(query.lowercase()) == true - collectionFulfillQuery || assetsFulfillQuery + isCollectionFulfillQuery || isAssetsFulfillQuery }, ) - is NFTCollections.Content.Error -> it.content + is NFTCollections.Content.Error -> collections.content }, ) } @@ -198,12 +166,12 @@ internal class NFTCollectionsModel @Inject constructor( } private fun onExpandCollectionClick(collection: NFTCollection) { - _state.update { + _state.update { stateUM -> ChangeCollectionExpandedStateTransformer( collection = collection, collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, - ).transform(it) + ).transform(stateUM) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 6c9581913e..0cbb637c1c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.PortfolioId import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent @@ -53,7 +52,6 @@ internal class DefaultNFTComponent @AssistedInject constructor( private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, private val portfolioSelectorController: PortfolioSelectorController, portfolioFetcherFactory: PortfolioFetcher.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -66,14 +64,10 @@ internal class DefaultNFTComponent @AssistedInject constructor( private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId) private val currentRoute = MutableStateFlow(initialRoute) private val onReceiveClickJob = JobHolder() - private val portfolioFetcher: PortfolioFetcher? = if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), - scope = componentScope, - ) - } else { - null - } + private val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = componentScope, + ) private val bottomSheetNavigation: SlotNavigation = SlotNavigation() private val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() } @@ -83,7 +77,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( source = bottomSheetNavigation, serializer = Unit.serializer(), handleBackButton = false, - childFactory = { configuration, context -> bottomSheetChild(context) }, + childFactory = { _, context -> bottomSheetChild(context) }, ) private val childStack = childStack( @@ -146,17 +140,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( params = NFTCollectionsComponent.Params( userWalletId = route.userWalletId, onBackClick = ::onChildBack, - onReceiveClick = { - if (accountsFeatureToggles.isFeatureEnabled) { - onReceiveClick(route) - } else { - innerRouter.push( - NFTRoute.Receive( - portfolioId = PortfolioId(route.userWalletId), - ), - ) - } - }, + onReceiveClick = { onReceiveClick(route) }, onAssetClick = { asset, collection -> innerRouter.push( NFTRoute.Details( @@ -170,7 +154,6 @@ internal class DefaultNFTComponent @AssistedInject constructor( ) private fun onReceiveClick(route: NFTRoute.Collections) = componentScope.launch { - val portfolioFetcher = requireNotNull(portfolioFetcher) portfolioSelectorController.selectAccount(null) portfolioFetcher.updateMode(mode = PortfolioFetcher.Mode.Wallet(route.userWalletId)) val portfolioData = portfolioFetcher.data.first() @@ -245,7 +228,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( portfolioSelectorComponentFactory.create( context = childByContext(componentContext), params = PortfolioSelectorComponent.Params( - portfolioFetcher = portfolioFetcher!!, + portfolioFetcher = portfolioFetcher, controller = portfolioSelectorController, bsCallback = portfolioSelectorCallback, ), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt index 53e6ba8d82..f6f073d569 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt @@ -1,15 +1,14 @@ package com.tangem.features.onboarding.v2.common.ui -import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference internal data class OnboardingDialogUM( - override val title: TextReference, - override val message: TextReference, + val title: TextReference, + val message: TextReference, val dismissButtonText: TextReference, - override val confirmButtonText: TextReference, + val confirmButtonText: TextReference, val dismissWarningColor: Boolean = false, - override val onConfirmClick: () -> Unit, + val onConfirmClick: () -> Unit, val onDismissButtonClick: () -> Unit, val onDismiss: () -> Unit, -) : AlertUM \ No newline at end of file +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index d4c055fcda..990778ca91 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -59,7 +59,7 @@ internal fun MultiWalletAccessCodeEnter( val focusRequester = remember { FocusRequester() } OutlineTextField( - modifier = modifier + modifier = Modifier .focusRequester(focusRequester) .fillMaxWidth(), value = if (reEnterAccessCodeState) { @@ -73,12 +73,14 @@ internal fun MultiWalletAccessCodeEnter( state.onAccessCodeFirstChange }, label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), - isError = state.codesNotMatchError, + isError = state.codesNotMatchError || state.atLeast4CharError, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) + state.atLeast4CharError && !reEnterAccessCodeState -> + stringResourceSafe(R.string.onboarding_access_code_too_short) else -> null }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index d070a4ffed..3fc4aa71fb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped @@ -262,6 +263,9 @@ class MultiWalletBackupModel @Inject constructor( errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), onRequestSupport = { modelScope.launch { + analyticsEventHandler.send( + Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding), + ) sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) } }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index d777faa37b..205405cad4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -5,6 +5,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -182,6 +183,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( modelScope.launch { val cardInfo = getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + analyticsHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding)) sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 0fb3c2f011..61d26b089e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -5,7 +5,9 @@ import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -71,6 +73,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val walletsRepository: WalletsRepository, private val uiMessageSender: UiMessageSender, private val backupValidator: BackupValidator, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -392,6 +395,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( modelScope.launch { val cardInfo = getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding)) sendFeedbackEmailUseCase(FeedbackEmailType.BackupProblem(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index fc3e5ad044..c960e1022a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -7,6 +7,7 @@ import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.core.TangemSdkError 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.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped @@ -310,6 +311,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( modelScope.launch { val cardInfo = getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + analyticsHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding)) sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt index d7e2c6e3e9..c5a2989d7c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt @@ -4,7 +4,9 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -48,6 +50,7 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -179,6 +182,7 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor( modelScope.launch { val cardInfo = getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding)) sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } 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 636c9f4f32..aa083e3575 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 @@ -8,6 +8,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.instancekeeper.getOrCreateSimple import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.context.AppComponentContext import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -45,6 +47,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch val userWalletId = cardInfo.userWalletId val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() } + analyticsHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding)) sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 2dbc11a45c..b1893158d7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -9,6 +9,7 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -358,6 +359,9 @@ internal class OnboardingTwinModel @Inject constructor( errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), onRequestSupport = { modelScope.launch { + analyticsEventHandler.send( + Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Onboarding), + ) sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) } }, diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index f0a6cb16c5..fcab39bc50 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -42,8 +42,8 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/features/onramp/impl/detekt-baseline-debug.xml b/features/onramp/impl/detekt-baseline-debug.xml deleted file mode 100644 index ecf2e0cce8..0000000000 --- a/features/onramp/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt index 2f3739e450..039af4534d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM internal interface AllOffersIntents { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 3dcc789481..ee7af4a393 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -10,9 +10,9 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.MINUS import kotlinx.collections.immutable.toImmutableList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt index 8e1847afc5..4807f6e61a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodStatus -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import kotlinx.collections.immutable.ImmutableList internal sealed interface AllOffersStateUM { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index b29663f8cc..5b0c5748d3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -12,7 +12,7 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersIntents import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.Job diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt index e59a517b99..beaf222881 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -32,10 +32,10 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.Offer +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.Offer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt index 46ca3d4017..5e40a22e41 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -33,10 +33,10 @@ import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.TimingBlock +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.TimingBlock import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt index feff4ba69b..1112152d60 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt @@ -31,7 +31,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.onramp.hottokens.model.HotCryptoModel import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent @@ -48,38 +47,29 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: HotCryptoComponent.Params, private val onrampAddToPortfolioComponentFactory: OnrampAddToPortfolioComponent.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, addTokenComponentFactory: OnrampAddTokenComponent.Factory, ) : HotCryptoComponent, AppComponentContext by context { private val model: HotCryptoModel = getOrCreateModel(params) - private val portfolioSelectorComponent: PortfolioSelectorComponent? by lazy { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioSelectorComponentFactory.create( - context = child("portfolioSelectorComponent"), - params = PortfolioSelectorComponent.Params( - portfolioFetcher = requireNotNull(model.portfolioFetcher), - controller = model.portfolioSelectorController, - ), - ) - } else { - null - } + private val portfolioSelectorComponent: PortfolioSelectorComponent by lazy { + portfolioSelectorComponentFactory.create( + context = child("portfolioSelectorComponent"), + params = PortfolioSelectorComponent.Params( + portfolioFetcher = model.portfolioFetcher, + controller = model.portfolioSelectorController, + ), + ) } - private val addTokenComponent: OnrampAddTokenComponent? by lazy { - if (accountsFeatureToggles.isFeatureEnabled) { - addTokenComponentFactory.create( - context = child("addTokenComponent"), - params = OnrampAddTokenComponent.Params( - callbacks = model, - tokenToAdd = model.hotCryptoToAddDataFlow, - ), - ) - } else { - null - } + private val addTokenComponent: OnrampAddTokenComponent by lazy { + addTokenComponentFactory.create( + context = child("addTokenComponent"), + params = OnrampAddTokenComponent.Params( + callbacks = model, + tokenToAdd = model.hotCryptoToAddDataFlow, + ), + ) } private val bottomSheetSlot = childSlot( @@ -106,9 +96,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( HotCrypto(state, modifier) bottomSheet.child?.instance?.BottomSheet() - if (accountsFeatureToggles.isFeatureEnabled) { - AddHotCryptoBottomSheet() - } + AddHotCryptoBottomSheet() } @Composable @@ -133,7 +121,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( content = TangemBottomSheetConfigContent.Empty, ), containerColor = TangemTheme.colors.background.tertiary, - title = { state -> + title = { _ -> AnimatedContent(targetState = contentStack.value) { stack -> BottomSheetTitle( stack = stack, @@ -142,7 +130,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( ) } }, - content = { state -> + content = { _ -> AnimatedContent(targetState = contentStack.value) { stack -> val paddingModifier = Modifier.padding( start = 16.dp, @@ -221,8 +209,8 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( } private fun contentChild(config: OnrampAddTokenRoute): ComposableContentComponent = when (config) { - OnrampAddTokenRoute.AddToken -> requireNotNull(addTokenComponent) - OnrampAddTokenRoute.PortfolioSelector -> requireNotNull(portfolioSelectorComponent) + OnrampAddTokenRoute.AddToken -> addTokenComponent + OnrampAddTokenRoute.PortfolioSelector -> portfolioSelectorComponent OnrampAddTokenRoute.Empty -> ComposableContentComponent.EMPTY } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index 40ac56570b..bf19ec5d37 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -1,9 +1,6 @@ package com.tangem.features.onramp.hottokens.model -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.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.popToFirst import com.arkivanov.decompose.router.stack.pushNew @@ -13,7 +10,6 @@ import com.tangem.blockchainsdk.utils.toCoinId 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.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.data.common.currency.getCoinId @@ -21,18 +17,11 @@ import com.tangem.data.common.currency.getTokenId import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.common.currency.isCustomToken import com.tangem.data.common.network.NetworkFactory -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus -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.UserWallet -import com.tangem.domain.onramp.GetHotCryptoUseCase import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorController import com.tangem.features.onramp.hottokens.HotCryptoComponent @@ -52,7 +41,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -60,9 +48,6 @@ import javax.inject.Inject * Hot crypto model * * @param paramsContainer params container - * @param getHotCryptoUseCase use case for getting hot crypto - * @param getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getCryptoCurrencyStatusSyncUseCase use case for getting crypto currency status by id * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -71,22 +56,17 @@ import javax.inject.Inject @ModelScoped internal class HotCryptoModel @Inject constructor( paramsContainer: ParamsContainer, - private val getHotCryptoUseCase: GetHotCryptoUseCase, private val callbackDelegate: HotCryptoModelCallbackDelegate, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, override val dispatchers: CoroutineDispatcherProvider, private val hotCryptoPortfolioDataLoader: HotCryptoPortfolioDataLoader, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, val portfolioSelectorController: PortfolioSelectorController, private val networkFactory: NetworkFactory, - private val portfolioFetcherFactory: PortfolioFetcher.Factory, + portfolioFetcherFactory: PortfolioFetcher.Factory, ) : Model(), OnrampAddTokenComponent.Callbacks by callbackDelegate { val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val portfolioFetcher: PortfolioFetcher? + val portfolioFetcher: PortfolioFetcher val bottomSheetNavigationV2 = StackNavigation() private val addHotCryptoJob = JobHolder() val hotCryptoToAddDataFlow: MutableSharedFlow = MutableSharedFlow( @@ -100,53 +80,30 @@ internal class HotCryptoModel @Inject constructor( private val params: HotCryptoComponent.Params = paramsContainer.require() init { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), - scope = modelScope, - ) - combineData() - } else { - portfolioFetcher = null - combineDataOld() - } + portfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = modelScope, + ) + combineData() } private fun combineData() { - combine( - flow = hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId), - flow2 = isAccountsModeEnabledUseCase.invoke(), - transform = { data, isAccountMode -> + hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId) + .map { data -> HotTokenItemStateConverter( appCurrency = data.appCurrency, - onItemClick = { tokenItemState, hotCryptoCurrency -> + onItemClick = { _, hotCryptoCurrency -> startAddTokenFlow(currency = hotCryptoCurrency, hotCryptoPortfolioData = data) }, ) .convertList(data.allHotCrypto) .map(TokensListItemUM::Token) - }, - ) + } .onEach { items -> state.update { HotCryptoUM(items = it.buildItems(items)) } } .flowOn(dispatchers.default) .launchIn(modelScope) } - private fun combineDataOld() { - combine( - flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }, - flow2 = getHotCryptoUseCase(params.userWalletId), - ) { appCurrency, currencies -> - HotTokenItemStateConverter(appCurrency = appCurrency, onItemClick = ::onTokenClick) - .convertList(currencies) - .map(TokensListItemUM::Token) - } - .onEach { items -> - state.update { HotCryptoUM(items = it.buildItems(items)) } - } - .launchIn(modelScope) - } - private fun HotCryptoUM.buildItems(items: List): ImmutableList = buildList { if (items.isNotEmpty()) { @@ -164,16 +121,6 @@ internal class HotCryptoModel @Inject constructor( ) } - private fun onTokenClick(tokenItemState: TokenItemState, currency: HotCryptoCurrency) { - bottomSheetNavigation.activate( - configuration = OnrampAddToPortfolioBSConfig.AddToPortfolio( - cryptoCurrency = currency.cryptoCurrency, - currencyIconState = tokenItemState.iconState, - onSuccessAdding = ::onSuccessAdding, - ), - ) - } - private fun startAddTokenFlow(currency: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) { hotCryptoToAddDataFlow.resetReplayCache() fun closeNavigationFlow() = bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.Empty) @@ -201,7 +148,7 @@ internal class HotCryptoModel @Inject constructor( bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.PortfolioSelector) val tokenToAddStateFlow = portfolioSelectorController - .selectedAccountWithData(requireNotNull(portfolioFetcher)) + .selectedAccountWithData(portfolioFetcher) .filterNotNull() .map { (_, selectedAccount) -> val cryptoCurrency = updateCryptoCurrency( @@ -247,20 +194,6 @@ internal class HotCryptoModel @Inject constructor( .saveIn(addHotCryptoJob) } - private fun onSuccessAdding(id: CryptoCurrency.ID) { - modelScope.launch { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( - userWalletId = params.userWalletId, - cryptoCurrencyId = id, - ) - .onRight { status -> - bottomSheetNavigation.dismiss() - params.onTokenClick(status) - } - .onLeft { Timber.d("Unable to get CryptoCurrencyStatus[$id]: $it") } - } - } - private fun setupPortfolioSelector(hotCrypto: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) { portfolioSelectorController.selectAccount(null) portfolioSelectorController.isEnabled.value = isEnabled@{ _, accountStatus -> @@ -277,9 +210,9 @@ internal class HotCryptoModel @Inject constructor( private fun updateCryptoCurrency( cryptoCurrency: CryptoCurrency, userWallet: UserWallet, - account: AccountStatus, + account: AccountStatus.CryptoPortfolio, ): CryptoCurrency? { - val derivationIndex = account.account.derivationIndex ?: return null + val derivationIndex = account.account.derivationIndex val blockchain = cryptoCurrency.network.toBlockchain() val network = networkFactory.create( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 66fa4f5693..9863a66e16 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -1,12 +1,11 @@ package com.tangem.features.onramp.hottokens.portfolio.model -import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM @@ -21,11 +20,10 @@ import javax.inject.Inject /** * Model for adding token to portfolio * - * @param paramsContainer params container - * @property dispatchers dispatchers - * @property derivePublicKeysUseCase use case for deriving public key - * @property addCryptoCurrenciesUseCase use case for adding crypto currency - * @property getUserWalletUseCase use case for getting user wallet by id + * @param paramsContainer params container + * @property dispatchers dispatchers + * @property manageCryptoCurrenciesUseCase use case for managing crypto currencies + * @property getUserWalletUseCase use case for getting user wallet by id * [REDACTED_AUTHOR] */ @@ -33,8 +31,7 @@ import javax.inject.Inject internal class OnrampAddToPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { @@ -72,21 +69,14 @@ internal class OnrampAddToPortfolioModel @Inject constructor( private fun onAddClick() { modelScope.launch { changeAddButtonProgressStatus(isProgress = true) - derivePublicKeysUseCase( - userWalletId = params.userWalletId, - currencies = listOf(params.cryptoCurrency), - ).getOrElse { throwable -> - Timber.e("Failed to derive public keys: $throwable") - changeAddButtonProgressStatus(isProgress = false) - } - - addCryptoCurrenciesUseCase( - userWalletId = params.userWalletId, - currency = params.cryptoCurrency, - ) + val accountId = AccountId.forMainCryptoPortfolio(params.userWalletId) + manageCryptoCurrenciesUseCase(accountId = accountId, add = params.cryptoCurrency) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } - .onLeft { changeAddButtonProgressStatus(isProgress = false) } + .onLeft { throwable -> + Timber.e("Failed to add crypto currency: $throwable") + changeAddButtonProgressStatus(isProgress = false) + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 42967bcc47..bfcf43a112 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -8,15 +8,16 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.alloffers.AllOffersComponent import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import com.tangem.features.onramp.main.ui.OnrampMainComponentContent -import com.tangem.features.onramp.providers.SelectProviderComponent +import com.tangem.features.onramp.main.ui.OnrampMainScreen import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -27,10 +28,15 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted private val params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val selectProviderComponentFactory: SelectProviderComponent.Factory, + private val allOffersComponentFactory: AllOffersComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) + + init { + lifecycle.subscribe(onStop = model::onStop) + } + private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, @@ -43,7 +49,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( val state by model.state.collectAsState() val bottomSheet by bottomSheetSlot.subscribeAsState() - OnrampMainComponentContent(modifier = modifier, state = state) + OnrampMainScreen(modifier = modifier, state = state) bottomSheet.child?.instance?.BottomSheet() } @@ -57,7 +63,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, country = config.country, - isLaunchSepa = params.isLaunchSepa, + isLaunchSepa = false, onDismiss = { model.bottomSheetNavigation.dismiss() model.handleOnrampAvailable() @@ -72,14 +78,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( onDismiss = model.bottomSheetNavigation::dismiss, ), ) - is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create( + is OnrampMainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( context = childByContext(componentContext), - params = SelectProviderComponent.Params( - onProviderClick = model::onProviderSelected, - onDismiss = model.bottomSheetNavigation::dismiss, - selectedProviderId = config.selectedProviderId, - selectedPaymentMethod = config.selectedPaymentMethod, + params = AllOffersComponent.Params( + userWallet = model.userWallet, cryptoCurrency = params.cryptoCurrency, + onDismiss = model.bottomSheetNavigation::dismiss, + openRedirectPage = params.openRedirectPage, + amountCurrencyCode = config.amountCurrencyCode, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index d4858314cf..98df5c2a8e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -15,7 +15,6 @@ internal interface OnrampMainComponent : ComposableContentComponent { val source: OnrampSource, val openSettings: () -> Unit, val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - val isLaunchSepa: Boolean, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt deleted file mode 100644 index f3bbdba606..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.main.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampMainComponentModel::class) - fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt index 78140491e5..440b6f0c7f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt @@ -1,11 +1,15 @@ package com.tangem.features.onramp.main.di +import com.tangem.core.decompose.model.Model import com.tangem.features.onramp.main.DefaultOnrampMainComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.model.OnrampMainComponentModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Singleton @Module @@ -15,4 +19,9 @@ internal interface OnrampMainComponentModule { @Binds @Singleton fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory + + @Binds + @IntoMap + @ClassKey(OnrampMainComponentModel::class) + fun bindOnrampMainComponentModel(model: OnrampMainComponentModel): Model } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt deleted file mode 100644 index c56ecedb67..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampAmountBlockUM( - val currencyUM: OnrampCurrencyUM, - val amountFieldModel: AmountFieldModel, - val secondaryFieldModel: OnrampAmountSecondaryFieldUM, -) - -internal data class OnrampCurrencyUM( - val code: String, - val iconUrl: String?, - val precision: Int, - val onClick: () -> Unit, -) - -@Immutable -internal sealed interface OnrampAmountSecondaryFieldUM { - data object Loading : OnrampAmountSecondaryFieldUM - data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM - data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt index 9ae8528504..729d275e25 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt @@ -1,17 +1,17 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -internal data class OnrampNewAmountBlockUM( - val currencyUM: OnrampNewCurrencyUM, +internal data class OnrampAmountBlockUM( + val currencyUM: OnrampCurrencyUM, val amountFieldModel: AmountFieldModel, val secondaryFieldModel: OnrampSecondaryFieldErrorUM, ) -internal data class OnrampNewCurrencyUM( +internal data class OnrampCurrencyUM( val unit: String, val code: String, val iconUrl: String?, @@ -25,9 +25,9 @@ internal sealed interface OnrampSecondaryFieldErrorUM { data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM } -internal sealed interface OnrampV2AmountButtonUMState { - data class Loaded(val amountButtons: ImmutableList) : OnrampV2AmountButtonUMState - data object None : OnrampV2AmountButtonUMState +internal sealed interface OnrampAmountButtonUMState { + data class Loaded(val amountButtons: ImmutableList) : OnrampAmountButtonUMState + data object None : OnrampAmountButtonUMState } internal data class OnrampAmountButtonUM( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 0115a77152..574c819d6a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -2,12 +2,15 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -interface OnrampIntents { - fun onAmountValueChanged(value: String, isValuePasted: Boolean) +internal interface OnrampIntents { + fun onAmountValueChanged(value: String) fun openSettings() fun openCurrenciesList() - fun onBuyClick(quote: OnrampProviderWithQuote.Data) + fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) fun openProviders() fun onRefresh() - fun onLinkClick(link: String) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt deleted file mode 100644 index 9bcc516c6b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampAmount -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -data class OnrampLastUpdate( - val fromAmount: OnrampAmount, - val countryCode: String, - val paymentMethod: OnrampPaymentMethod, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 2b0cff2f34..a317b2a287 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -1,11 +1,10 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampCountry -import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.serialization.Serializable @Serializable -internal sealed interface OnrampMainBottomSheetConfig { +sealed interface OnrampMainBottomSheetConfig { @Serializable data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig @@ -13,8 +12,5 @@ internal sealed interface OnrampMainBottomSheetConfig { data object CurrenciesList : OnrampMainBottomSheetConfig @Serializable - data class ProvidersList( - val selectedProviderId: String, - val selectedPaymentMethod: OnrampPaymentMethod, - ) : OnrampMainBottomSheetConfig + data class AllOffers(val amountCurrencyCode: String) : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 72fa50f884..b9e86bee09 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -4,55 +4,29 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.onramp.impl.R @Immutable internal sealed interface OnrampMainComponentUM { val topBarConfig: OnrampMainTopBarUM - val buyButtonConfig: BuyButtonConfig val errorNotification: NotificationUM? data class InitialLoading( - val currency: String, - val onClose: () -> Unit, - val openSettings: () -> Unit, - override val errorNotification: NotificationUM? = null, - ) : OnrampMainComponentUM { - override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Back( - onBackClicked = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ) - - override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( - text = resourceReference(R.string.common_buy), - onClick = {}, - isEnabled = false, - ) - } + override val topBarConfig: OnrampMainTopBarUM, + override val errorNotification: NotificationUM?, + ) : OnrampMainComponentUM data class Content( override val topBarConfig: OnrampMainTopBarUM, - override val buyButtonConfig: BuyButtonConfig, override val errorNotification: NotificationUM?, val amountBlockState: OnrampAmountBlockUM, - val providerBlockState: OnrampProviderBlockUM, + val offersBlockState: OnrampOffersBlockUM, + val onrampAmountButtonUMState: OnrampAmountButtonUMState, ) : OnrampMainComponentUM } -internal data class BuyButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - val isEnabled: Boolean, +internal data class OnrampMainTopBarUM( + val title: TextReference, + val startButtonUM: TopAppBarButtonUM, + val endButtonUM: TopAppBarButtonUM, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt deleted file mode 100644 index 5cd08fe2fa..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampMainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt similarity index 97% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt index aecbc43ee9..978aed9131 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt deleted file mode 100644 index 5434116a79..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed class OnrampProviderBlockUM { - data object Empty : OnrampProviderBlockUM() - data object Loading : OnrampProviderBlockUM() - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - val providerName: String, - val termsOfUseLink: String?, - val privacyPolicyLink: String?, - val isBestRate: Boolean, - val onLinkClick: (String) -> Unit, - val onClick: () -> Unit, - ) : OnrampProviderBlockUM() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt new file mode 100644 index 0000000000..dcbc368282 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.domain.onramp.model.OnrampPaymentMethod + +sealed interface OnrampProvidersUM { + + data object Empty : OnrampProvidersUM + + data object Loading : OnrampProvidersUM + + data class Content( + val providerId: String, + val paymentMethod: OnrampPaymentMethod, + ) : OnrampProvidersUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt similarity index 77% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt index 34902a7683..ce0cba166b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt @@ -1,24 +1,24 @@ -package com.tangem.features.onramp.mainv2.entity.converter +package com.tangem.features.onramp.main.entity.converter import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal -internal class OnrampV2AmountFieldChangeConverter( - private val currentStateProvider: Provider, +internal class OnrampAmountFieldChangeConverter( + private val currentStateProvider: Provider, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val onrampIntents: OnrampV2Intents, -) : Converter { + private val onrampIntents: OnrampIntents, +) : Converter { - override fun convert(value: String): OnrampV2MainComponentUM { + override fun convert(value: String): OnrampMainComponentUM { val state = currentStateProvider() - if (state !is OnrampV2MainComponentUM.Content) return state + if (state !is OnrampMainComponentUM.Content) return state if (value.isEmpty()) return state.emptyState() @@ -36,13 +36,13 @@ internal class OnrampV2AmountFieldChangeConverter( amountFieldModel = amountFieldModel, secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, offersBlockState = OnrampOffersBlockUM.Loading, errorNotification = null, ) } - private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content { + private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { val amountFieldModel = amountBlockState.amountFieldModel.copy( value = "", fiatValue = "", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt index 4bf1a7285a..27b5cdabd5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt @@ -1,7 +1,7 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState import kotlinx.collections.immutable.toPersistentList internal class OnrampAmountButtonUMStateFactory { @@ -12,7 +12,7 @@ internal class OnrampAmountButtonUMStateFactory { currencyCode: String, currencySymbol: String, onAmountValueChanged: (String) -> Unit, - ): OnrampV2AmountButtonUMState { + ): OnrampAmountButtonUMState { return when (currencyCode) { USD_CODE, EUR_CODE -> { val buttons = defaultPreselectedAmount.map { value -> @@ -22,9 +22,9 @@ internal class OnrampAmountButtonUMStateFactory { onClick = { onAmountValueChanged(value.toString()) }, ) }.toPersistentList() - OnrampV2AmountButtonUMState.Loaded(buttons) + OnrampAmountButtonUMState.Loaded(buttons) } - else -> OnrampV2AmountButtonUMState.None + else -> OnrampAmountButtonUMState.None } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt index a91f463964..462c27438a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.resourceReference @@ -11,34 +11,34 @@ import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.model.AmountType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.converter.OnrampAmountFieldChangeConverter import com.tangem.utils.Provider -internal class OnrampV2AmountStateFactory( - private val currentStateProvider: Provider, +internal class OnrampAmountStateFactory( + private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampV2Intents, + private val onrampIntents: OnrampIntents, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, ) { - private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy( + private val onrampAmountFieldChangeConverter: OnrampAmountFieldChangeConverter by lazy( mode = LazyThreadSafetyMode.NONE, ) { - OnrampV2AmountFieldChangeConverter( + OnrampAmountFieldChangeConverter( currentStateProvider = currentStateProvider, onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, onrampIntents = onrampIntents, ) } - fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM { + fun getOnAmountValueChange(value: String): OnrampMainComponentUM { return onrampAmountFieldChangeConverter.convert(value) } - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM { + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState @@ -72,9 +72,9 @@ internal class OnrampV2AmountStateFactory( ) } - fun getSecondaryFieldAmountErrorState(quotes: List): OnrampV2MainComponentUM { + fun getSecondaryFieldAmountErrorState(quotes: List): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState @@ -91,23 +91,23 @@ internal class OnrampV2AmountStateFactory( ) } - fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM { + fun getAmountSecondaryFieldResetState(): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, errorNotification = null, offersBlockState = currentState.offersBlockState, ) } private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampNewAmountBlockUM, + amountState: OnrampAmountBlockUM, ): OnrampSecondaryFieldErrorUM.Error { val amount = error.requiredAmount.format { fiat( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt similarity index 89% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt index c16f800f59..889f62feca 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt @@ -1,24 +1,23 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.onramp.model.* import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList internal class OnrampOffersStateFactory( - private val currentStateProvider: Provider, - private val onrampIntents: OnrampV2Intents, + private val currentStateProvider: Provider, + private val onrampIntents: OnrampIntents, ) { - fun getOffersState(offers: List): OnrampV2MainComponentUM { - val currentState = currentStateProvider.invoke() - return when (currentState) { - is OnrampV2MainComponentUM.InitialLoading -> currentState - is OnrampV2MainComponentUM.Content -> { + fun getOffersState(offers: List): OnrampMainComponentUM { + return when (val currentState = currentStateProvider.invoke()) { + is OnrampMainComponentUM.InitialLoading -> currentState + is OnrampMainComponentUM.Content -> { if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) { return currentState } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 8faf435657..1d19b99737 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -8,7 +8,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError @@ -22,16 +24,30 @@ import java.math.BigDecimal internal class OnrampStateFactory( private val currentStateProvider: Provider, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, private val cryptoCurrency: CryptoCurrency, private val onrampIntents: OnrampIntents, ) { - fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading { + fun getInitialState( + currency: String, + onClose: () -> Unit, + openSettings: () -> Unit, + ): OnrampMainComponentUM.InitialLoading { return OnrampMainComponentUM.InitialLoading( - currency = currency, - onClose = onClose, - openSettings = onrampIntents::openSettings, errorNotification = null, + topBarConfig = OnrampMainTopBarUM( + title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), + startButtonUM = TopAppBarButtonUM.Close( + onCloseClick = onClose, + enabled = true, + ), + endButtonUM = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_more_vertical_24, + onClicked = openSettings, + isEnabled = false, + ), + ), ) } @@ -42,12 +58,19 @@ internal class OnrampStateFactory( is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) } + + val initialAmountBlockState = getInitialAmountBlockState(currency) + return OnrampMainComponentUM.Content( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig, - amountBlockState = getInitialAmountBlockState(currency), - providerBlockState = OnrampProviderBlockUM.Empty, + amountBlockState = initialAmountBlockState, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = null, + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencyCode = currency.code, + currencySymbol = currency.unit, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), ) } @@ -68,21 +91,6 @@ internal class OnrampStateFactory( } } - private fun getNoPairsErrorState(): OnrampMainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampMainComponentUM.Content ?: return state - - return contentState.copy( - buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false), - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - ) - } - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() val endButton = when (val button = state.topBarConfig.endButtonUM) { @@ -93,15 +101,15 @@ internal class OnrampStateFactory( return when (state) { is OnrampMainComponentUM.Content -> state.copy( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false), - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - providerBlockState = OnrampProviderBlockUM.Empty, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, onRefresh = onRefresh, ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + amountBlockState = state.amountBlockState.copy( + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, + ), ) is OnrampMainComponentUM.InitialLoading -> state.copy( errorNotification = NotificationUM.Warning.OnrampErrorNotification( @@ -112,6 +120,22 @@ internal class OnrampStateFactory( } } + private fun getNoPairsErrorState(): OnrampMainComponentUM { + val state = currentStateProvider() + val contentState = state as? OnrampMainComponentUM.Content ?: return state + + return contentState.copy( + amountBlockState = contentState.amountBlockState.copy( + amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( + error = resourceReference(R.string.onramp_no_available_providers), + ), + ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, + ) + } + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM { return OnrampAmountBlockUM( currencyUM = OnrampCurrencyUM( @@ -119,11 +143,12 @@ internal class OnrampStateFactory( iconUrl = currency.image, precision = currency.precision, onClick = onrampIntents::openCurrenciesList, + unit = currency.unit, ), amountFieldModel = AmountFieldModel( value = "", fiatValue = "", - onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) }, + onValueChange = onrampIntents::onAmountValueChanged, keyboardOptions = KeyboardOptions( imeAction = ImeAction.None, keyboardType = KeyboardType.Number, @@ -139,7 +164,7 @@ internal class OnrampStateFactory( isValuePasted = false, onValuePastedTriggerDismiss = {}, ), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ) } @@ -149,8 +174,4 @@ internal class OnrampStateFactory( decimals = currency.precision, type = AmountType.FiatType(currency.code), ) - - companion object { - const val PREDEFINED_SEPA_AMOUNT = "100" - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt deleted file mode 100644 index ef9cff01ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal - -internal class OnrampAmountFieldChangeConverter( - private val currentStateProvider: Provider, -) : Converter { - - override fun convert(input: Input): OnrampMainComponentUM { - val value = input.value - val isValuePasted = input.isValuePasted - - val state = currentStateProvider() - if (state !is OnrampMainComponentUM.Content) return state - - if (value.isEmpty()) return state.emptyState() - - val amountState = state.amountBlockState - val amountTextField = amountState.amountFieldModel - val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO - val isDoneActionEnabled = !fiatDecimal.isNullOrZero() - val amountFieldModel = amountState.amountFieldModel.copy( - fiatValue = value, - fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - isValuePasted = isValuePasted, - ) - - return state.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, - ), - providerBlockState = OnrampProviderBlockUM.Loading, - ) - } - - private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { - val amountFieldModel = amountBlockState.amountFieldModel.copy( - value = "", - fiatValue = "", - cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), - fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), - isError = false, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ) - return copy( - amountBlockState = amountBlockState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - buyButtonConfig = buyButtonConfig.copy(isEnabled = false), - providerBlockState = OnrampProviderBlockUM.Empty, - ) - } - - data class Input(val value: String, val isValuePasted: Boolean) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt deleted file mode 100644 index 24010b69e9..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.AmountType -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.* -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.utils.Provider -import com.tangem.utils.extensions.isSingleItem - -internal class OnrampAmountStateFactory( - private val currentStateProvider: Provider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampIntents, - private val cryptoCurrency: CryptoCurrency, - private val needApplyFCARestrictions: Provider, -) { - - private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( - currentStateProvider = currentStateProvider, - ) - - fun getOnAmountValueChange(value: String, isValuePasted: Boolean) = - onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted)) - - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - return currentState.copy( - amountBlockState = amountState.copy( - currencyUM = amountState.currencyUM.copy( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - ), - amountFieldModel = amountState.amountFieldModel.copy( - isError = false, - fiatAmount = amountState.amountFieldModel.fiatAmount.copy( - currencySymbol = currency.unit, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ), - ), - ), - ) - } - - fun getAmountSecondaryLoadingState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), - providerBlockState = OnrampProviderBlockUM.Loading, - buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false), - errorNotification = null, - ) - } - - fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountState.amountFieldModel.copy(isError = false), - secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = quote is OnrampQuote.Data, - onClick = { - if (quote is OnrampQuote.Data) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = quote.provider, - paymentMethod = quote.paymentMethod, - toAmount = quote.toAmount, - fromAmount = quote.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - analyticsEventHandler.send( - OnrampAnalyticsEvent.ProviderCalculated( - providerName = selectedQuote.provider.info.name, - tokenSymbol = cryptoCurrency.symbol, - paymentMethod = selectedQuote.paymentMethod.name, - ), - ) - - val bestProvider = quotes.firstOrNull() - val isMultipleQuotes = !quotes.isSingleItem() - val isOtherQuotesHasData = quotes - .filter { it.paymentMethod == selectedQuote.paymentMethod } - .filterNot { it == bestProvider } - .any { it is OnrampQuote.Data } - - val isBestProvider = selectedQuote == bestProvider && - isMultipleQuotes && - isOtherQuotesHasData && - !needApplyFCARestrictions() - - return currentState.copy( - providerBlockState = selectedQuote.toProviderBlockState(isBestProvider), - ) - } - - fun getAmountSecondaryUpdatedState( - providerResult: SelectProviderResult, - isBestRate: Boolean, - ): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - val secondaryField = when (providerResult) { - is SelectProviderResult.ProviderWithError -> { - providerResult.quoteError.toSecondaryFieldUiModel(amountState) - } - is SelectProviderResult.ProviderWithQuote -> { - val amount = providerResult.toAmount.value.format { - crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - } - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = secondaryField), - providerBlockState = OnrampProviderBlockUM.Content( - paymentMethod = providerResult.paymentMethod, - providerId = providerResult.provider.id, - providerName = providerResult.provider.info.name, - isBestRate = isBestRate && !needApplyFCARestrictions(), - onClick = onrampIntents::openProviders, - termsOfUseLink = providerResult.provider.info.termsOfUseLink, - privacyPolicyLink = providerResult.provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = providerResult is SelectProviderResult.ProviderWithQuote, - onClick = { - if (providerResult is SelectProviderResult.ProviderWithQuote) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = providerResult.provider, - paymentMethod = providerResult.paymentMethod, - toAmount = providerResult.toAmount, - fromAmount = providerResult.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getAmountSecondaryResetState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - if (amountState.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Content) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content( - amount = TextReference.EMPTY, - ), - ), - errorNotification = null, - ) - } - - private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM { - return OnrampProviderBlockUM.Content( - paymentMethod = paymentMethod, - providerId = provider.id, - providerName = provider.info.name, - isBestRate = isBestRate, - onClick = onrampIntents::openProviders, - termsOfUseLink = provider.info.termsOfUseLink, - privacyPolicyLink = provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ) - } - - private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? { - return when (this) { - is OnrampQuote.Error -> null - is OnrampQuote.Data -> { - val amount = toAmount.value.format { - crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) - } - } - - private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampAmountBlockUM, - ): OnrampAmountSecondaryFieldUM.Error { - val amount = error.requiredAmount.format { - fiat( - fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, - fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, - ) - } - - val errorTextRes = when (error) { - is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) - R.string.onramp_max_amount_restriction - } - is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) - R.string.onramp_min_amount_restriction - } - } - - return OnrampAmountSecondaryFieldUM.Error( - resourceReference( - errorTextRes, - wrappedList(amount), - ), - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 7c501be535..b72f54919d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -1,38 +1,25 @@ package com.tangem.features.onramp.main.model -import androidx.compose.runtime.mutableStateOf import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.fields.InputManager -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError -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.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory -import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT -import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.features.onramp.utils.model.EUR_CURRENCY import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,7 +30,6 @@ import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -51,85 +37,141 @@ internal class OnrampMainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val router: Router, - private val isDemoCardUseCase: IsDemoCardUseCase, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, private val getOnrampCountryUseCase: GetOnrampCountryUseCase, private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase, - private val onrampGetDefaultCurrencyUseCase: OnrampGetDefaultCurrencyUseCase, private val amountInputManager: InputManager, - private val messageSender: UiMessageSender, - private val urlOpener: UrlOpener, - getWalletsUseCase: GetWalletsUseCase, - getUserCountryUseCase: GetUserCountryUseCase, + private val getOnrampOffersUseCase: GetOnrampOffersUseCase, paramsContainer: ParamsContainer, + getWalletsUseCase: GetWalletsUseCase, ) : Model(), OnrampIntents { - private val params: OnrampMainComponent.Params = paramsContainer.require() + val params = paramsContainer.require() - private var shouldForceChooseSepa = params.isLaunchSepa - private var currencyToRestore: OnrampCurrency? = null - - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val lastUpdateState = mutableStateOf(null) - private var userCountry: UserCountry? = null + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountButtonUMStateFactory() + } @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory = OnrampStateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - ) + private val stateFactory: OnrampStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampStateFactory( + currentStateProvider = Provider { state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } val state: StateFlow field = MutableStateFlow( value = stateFactory.getInitialState( currency = params.cryptoCurrency.name, onClose = ::onCloseClick, + openSettings = ::openSettings, ), ) - private val amountStateFactory = OnrampAmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - cryptoCurrency = params.cryptoCurrency, - needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() }, - ) + private val amountStateFactory: OnrampAmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountStateFactory( + currentStateProvider = Provider { state.value }, + analyticsEventHandler = analyticsEventHandler, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } + + private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampOffersStateFactory( + currentStateProvider = Provider { state.value }, + onrampIntents = this, + ) + } private val quotesTaskScheduler = SingleTaskScheduler() - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + init { modelScope.launch { clearOnrampCacheUseCase() - - if (params.isLaunchSepa) { - currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull() - onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY) - } } - + startLoadingQuotes() sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() + subscribeToCountryAndCurrencyUpdates() + subscribeToQuotesUpdate() + subscribeOnOffers() } - private fun sendScreenOpenAnalytics() { + override fun onDestroy() { + modelScope.launch { clearOnrampCacheUseCase.invoke() } + quotesTaskScheduler.cancelTask() + super.onDestroy() + } + + override fun onAmountValueChanged(value: String) { + state.update { amountStateFactory.getOnAmountValueChange(value) } + modelScope.launch { amountInputManager.update(value) } + } + + override fun openSettings() { + params.openSettings.invoke() + } + + override fun openCurrenciesList() { + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) + } + + override fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, + OnrampAnalyticsEvent.OnBuyClick( + providerName = quote.provider.info.name, + currency = currentContentState.amountBlockState.currencyUM.code, tokenSymbol = params.cryptoCurrency.symbol, ), ) + sendOfferClickEvent( + quote = quote, + onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, + categoryUM = categoryUM, + ) + params.openRedirectPage(quote) + } + + override fun openProviders() { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return + val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.AllOffers(amountCurrentCode)) + } + + override fun onRefresh() { + state.update { + stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = router::pop, + openSettings = ::openSettings, + ) + } + modelScope.launch { + clearOnrampCacheUseCase.invoke() + checkResidenceCountry() + handleOnrampAvailable() + } + } + + fun onStop() { + quotesTaskScheduler.cancelTask() } fun handleOnrampAvailable() { @@ -137,92 +179,6 @@ internal class OnrampMainComponentModel @Inject constructor( subscribeToQuotesUpdate() } - fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) { - state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) } - - if (result.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> handleOnrampAvailable() - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - - val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading - state.update { prevState -> - if (prevState is OnrampMainComponentUM.InitialLoading) { - stateFactory.getReadyState(country.defaultCurrency) - } else { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - } - - updatePairsAndQuotes() - - if (wasInitialLoading && params.isLaunchSepa) { - onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true) - } - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - state.update { amountStateFactory.getAmountSecondaryLoadingState() } - startLoadingQuotes() - } - } - - private suspend fun updatePairsAndQuotes() { - state.update { prevState -> - val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState - - if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) { - amountStateFactory.getAmountSecondaryLoadingState() - } else { - prevState - } - } - - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } }, - ) - startLoadingQuotes() - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - sendOnrampErrorAnalytic(onrampError) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - private fun startLoadingQuotes() { quotesTaskScheduler.cancelTask() quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) @@ -233,11 +189,12 @@ internal class OnrampMainComponentModel @Inject constructor( delay = UPDATE_DELAY, task = { runSuspendCatching { - val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching - val amountBlockState = content.amountBlockState - if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) { - return@runSuspendCatching - } + val amountBlockState = (state.value as? OnrampMainComponentUM.Content)?.amountBlockState + ?: return@runSuspendCatching + + val fiatAmount = amountBlockState.amountFieldModel.fiatAmount + if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching + fetchQuotesUseCase.invoke( userWallet = userWallet, amount = amountBlockState.amountFieldModel.fiatAmount, @@ -250,6 +207,84 @@ internal class OnrampMainComponentModel @Inject constructor( ) } + private fun checkResidenceCountry() { + modelScope.launch { + checkOnrampAvailabilityUseCase(userWallet) + .onRight(::handleOnrampAvailability) + .onLeft(::handleOnrampError) + } + } + + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> Unit + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) + } + } + + private fun onCloseClick() { + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) + router.pop() + } + + private fun subscribeOnOffers() = modelScope.launch { + getOnrampOffersUseCase + .invoke() + .collectLatest { maybeOffers -> + maybeOffers.fold( + ifLeft = ::handleOnrampError, + ifRight = { offers -> + val currentState = state.value + if (currentState is OnrampMainComponentUM.Content) { + if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { + state.update { + currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } + return@fold + } + state.update { + onrampOffersStateFactory.getOffersState(offers) + } + } + }, + ) + } + } + + private fun subscribeToAmountChanges() = modelScope.launch { + amountInputManager.query + .filter(String::isNotEmpty) + .collectLatest { _ -> + startLoadingQuotes() + } + } + + private fun subscribeToCountryAndCurrencyUpdates() { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + maybeCountry.fold( + ifLeft = ::handleOnrampError, + ifRight = { country -> + if (country == null) return@onEach + state.update { prevState -> + when (prevState) { + is OnrampMainComponentUM.Content -> { + amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + } + is OnrampMainComponentUM.InitialLoading -> { + stateFactory.getReadyState(country.defaultCurrency) + } + } + } + updatePairsAndQuotes() + }, + ) + } + .launchIn(modelScope) + } + private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .conflate() @@ -262,152 +297,31 @@ internal class OnrampMainComponentModel @Inject constructor( .launchIn(modelScope) } - override fun onAmountValueChanged(value: String, isValuePasted: Boolean) { - state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings() - } - - override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { - if (userWallet is UserWallet.Cold && isDemoCardUseCase.invoke(userWallet.cardId)) { - showDemoWarning() - } else { - val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - params.openRedirectPage(quote) - } - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) - } - - override fun openProviders() { - val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return - val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return - bottomSheetNavigation.activate( - OnrampMainBottomSheetConfig.ProvidersList( - selectedPaymentMethod = providerContentState.paymentMethod, - selectedProviderId = providerContentState.providerId, - ), - ) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - ) - } - quotesTaskScheduler.cancelTask() - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - } - } - - override fun onLinkClick(link: String) = urlOpener.openUrl(link) - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - - modelScope.launch { - if (params.isLaunchSepa) { - currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) } - } - } - - super.onDestroy() - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - private fun handleQuoteResult(quotes: List) { sendOnrampQuotesErrorAnalytic(quotes) - - val quote = selectOrUpdateQuote(quotes) - - if (quote == null) { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - lastUpdateState.value = null - return - } - state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } - } - - /** - * !!! Important quote selection logic !!! - * Selects or updated quote based on input data (amount, country, currency). - * If input data has changed select new best quote, otherwise last selected quote. - * If last selected quote on same input data is in an error state, select next best quote - * If new best quote or next best quote does not exist (i.e. Error state) select nothing. - */ - private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { - val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } - - val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) { - quotes.filterIsInstance() - .filter { it.paymentMethod.id == SEPA_METHOD_ID } - .maxByOrNull { it.toAmount.value } - } else { - null - } - - // Check if amount, country or currency has changed - val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) { - quoteToCheck - } else { - val state = state.value as? OnrampMainComponentUM.Content - val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content - - // Get current selected quote to update - val lastSelectedQuote = quotes.firstOrNull { quote -> - quote.provider.id == providerState?.providerId && - quote.paymentMethod.id == providerState.paymentMethod.id + when { + quotes.isEmpty() -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } } - - // Check if selected updated quote is not error - if (lastSelectedQuote is OnrampQuote.Error) { - quoteToCheck - } else { - lastSelectedQuote + quotes.all { it is OnrampQuote.AmountError } -> { + state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } + } + quotes.none { it is OnrampQuote.Data } -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + } + else -> { + state.update { prevState -> + val resetState = amountStateFactory.getAmountSecondaryFieldResetState() + if (prevState is OnrampMainComponentUM.Content && + resetState is OnrampMainComponentUM.Content && + prevState.offersBlockState is OnrampOffersBlockUM.Loading + ) { + resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } else { + resetState + } + } } - } - if (newQuote != null) { - updateProvider(newQuote, quotes) - } - - return newQuote - } - - private fun updateProvider(quote: OnrampQuote, quotes: List) { - lastUpdateState.value = OnrampLastUpdate( - quote.fromAmount, - quote.countryCode, - quote.paymentMethod, - ) - - if (quote.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - - state.update { - amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) } } @@ -415,41 +329,30 @@ internal class OnrampMainComponentModel @Inject constructor( state.update { prevState -> (prevState as? OnrampMainComponentUM.Content)?.copy( errorNotification = null, - providerBlockState = OnrampProviderBlockUM.Loading, + offersBlockState = OnrampOffersBlockUM.Loading, amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), ) ?: prevState } startLoadingQuotes() } - private fun showDemoWarning() { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) + private suspend fun updatePairsAndQuotes() { + fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( + ifLeft = ::handleOnrampError, + ifRight = { + state.update { + amountStateFactory.getAmountSecondaryFieldResetState() + } + startLoadingQuotes() }, - secondActionBuilder = { cancelAction() }, ) - - messageSender.send(message) } - private fun sendOnrampErrorAnalytic(error: OnrampError) { - val content = state.value as? OnrampMainComponentUM.Content - val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content - analyticsEventHandler.sendOnrampErrorEvent( - error = error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = providerContent?.providerName, - paymentMethod = providerContent?.paymentMethod?.name, - ) + private fun handleOnrampError(onrampError: OnrampError) { + Timber.e(onrampError.toString()) + state.update { stateFactory.getOnrampErrorState(onrampError) } } private fun sendOnrampQuotesErrorAnalytic(quotes: List) { @@ -467,20 +370,48 @@ internal class OnrampMainComponentModel @Inject constructor( providerName = errorState.provider.info.name, paymentMethod = errorState.paymentMethod.name, ) - else -> { /* no-op */ - } + else -> Unit } } } - private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean { - return lastUpdateState.value?.fromAmount != quote?.fromAmount || - lastUpdateState.value?.countryCode != quote?.countryCode + private fun sendScreenOpenAnalytics() { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ScreenOpened( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + } + + private fun sendOfferClickEvent( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val event = when (categoryUM) { + OnrampOfferCategoryUM.RecentlyUsed -> { + OnrampAnalyticsEvent.RecentlyBuyClicked( + tokenSymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethod = quote.paymentMethod.name, + ) + } + OnrampOfferCategoryUM.Recommended -> { + onrampOfferAdvantagesUM.toAnalyticsEvent( + cryptoCurrencySymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethodName = quote.paymentMethod.name, + ) + } + } + + if (event != null) { + analyticsEventHandler.send(event) + } } private companion object { const val UPDATE_DELAY = 10_000L - - const val SEPA_METHOD_ID = "sepa" } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 0bdfa02446..c2293646bd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.main.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,61 +19,89 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM import com.tangem.features.onramp.main.entity.OnrampCurrencyUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM @Composable -internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) { +internal fun OnrampAmountContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + ) + .padding(vertical = 24.dp, horizontal = 16.dp) + .animateContentSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - OnrampCurrencyIcon(currencyUM = state.currencyUM) - OnrampAmountField(amountField = state.amountFieldModel) - OnrampAmountSecondary(state = state.secondaryFieldModel) + OnrampHeaderTitle() + + OnrampAmountField( + amountField = state.amountBlockState.amountFieldModel, + currencyCode = state.amountBlockState.currencyUM.code, + ) + + AnimatedVisibility( + visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, + ) { + if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { + OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + } + } + + SpacerH(20.dp) + + OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) } } @Composable -private fun OnrampAmountField(amountField: AmountFieldModel) { +private fun OnrampHeaderTitle() { + Text( + text = stringResourceSafe(R.string.onramp_you_will_pay_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { val decimalFormat = rememberDecimalFormat() val requester = remember { FocusRequester() } - val symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - } AmountTextField( value = amountField.fiatValue, decimals = amountField.fiatAmount.decimals, visualTransformation = AmountVisualTransformation( decimals = amountField.fiatAmount.decimals, - symbol = amountField.fiatAmount.currencySymbol, - currencyCode = amountField.fiatAmount.currencySymbol, + symbol = currencyCode, + currencyCode = currencyCode, decimalFormat = decimalFormat, - symbolColor = symbolColor, + symbolColor = if (amountField.fiatValue.isBlank()) { + TangemTheme.colors.text.disabled + } else { + TangemTheme.colors.text.primary1 + }, ), onValueChange = amountField.onValueChange, keyboardOptions = amountField.keyboardOptions, keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( + textStyle = TangemTheme.typography.head.copy( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), @@ -82,7 +112,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { modifier = Modifier .focusRequester(requester) .padding( - top = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) @@ -96,7 +127,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { } @Composable -private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { +private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { Box( modifier = Modifier .fillMaxWidth() @@ -107,24 +138,12 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { ), contentAlignment = Alignment.Center, ) { - when (state) { - is OnrampAmountSecondaryFieldUM.Content -> Text( - text = state.amount.resolveReference(), - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Error -> Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(TangemTheme.dimens.size62), - ) - } + Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) } } @@ -132,20 +151,27 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) { Row( modifier = modifier - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.button.secondary) .clickable(onClick = currencyUM.onClick) - .padding(start = TangemTheme.dimens.spacing24), + .padding(horizontal = 6.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { AsyncImage( modifier = Modifier - .size(TangemTheme.dimens.size40) + .size(20.dp) .clip(CircleShape) .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), model = currencyUM.iconUrl, contentDescription = null, ) + Text( + text = currencyUM.code, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), + textAlign = TextAlign.Center, + ) Icon( modifier = Modifier .size(TangemTheme.dimens.size16) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt deleted file mode 100644 index 11e356cfef..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.tangem.features.onramp.main.ui - -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.* -import androidx.compose.foundation.text.ClickableText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.appendColored -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM - -private const val TERMS_OF_USE_KEY = "termsOfUse" -private const val PRIVACY_POLICY_KEY = "privacyPolicy" - -@Composable -internal fun OnrampButtonComponent(state: OnrampMainComponentUM) { - val content = state as? OnrampMainComponentUM.Content - val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content - Column( - modifier = Modifier - .navigationBarsPadding() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - OnrampTosText(providerState) - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_buy), - onClick = state.buyButtonConfig.onClick, - enabled = state.buyButtonConfig.isEnabled, - ) - } -} - -@Composable -private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { - val termsOfUse = stringResourceSafe(R.string.common_terms_of_use) - val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy) - val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy) - - val clickableAnnotation = buildAnnotatedString { - append(tosText.substringBefore(termsOfUse)) - - pushStringAnnotation(TERMS_OF_USE_KEY, "") - appendColored(termsOfUse, TangemTheme.colors.text.accent) - pop() - - append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy)) - - pushStringAnnotation(PRIVACY_POLICY_KEY, "") - appendColored(privacyPolicy, TangemTheme.colors.text.accent) - pop() - } - - AnimatedContent( - targetState = provider, - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Onramp Legal Info Animation", - ) { state -> - val termsOfUseLink = provider?.termsOfUseLink - val privacyPolicyLink = provider?.privacyPolicyLink - - if (state != null && termsOfUseLink != null && privacyPolicyLink != null) { - ClickableText( - text = clickableAnnotation, - style = TangemTheme.typography.caption2.copy( - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ), - onClick = { offset -> - val tosAnnotations = clickableAnnotation.getStringAnnotations( - tag = TERMS_OF_USE_KEY, - start = offset, - end = offset, - ) - - if (tosAnnotations.any()) { - state.onLinkClick(termsOfUseLink) - } - - val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations( - tag = PRIVACY_POLICY_KEY, - start = offset, - end = offset, - ) - - if (privacyPolicyAnnotations.any()) { - state.onLinkClick(privacyPolicyLink) - } - }, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt index f2ecc446d7..6b79790e18 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -20,13 +20,13 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM @Composable -internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { +internal fun BoxScope.OnrampFooterContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier .imePadding() @@ -53,16 +53,16 @@ internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content } @Composable -private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { +private fun OnrampAmountButtons(state: OnrampAmountButtonUMState) { val keyboard by keyboardAsState() AnimatedVisibility( - visible = state is OnrampV2AmountButtonUMState.Loaded, + visible = state is OnrampAmountButtonUMState.Loaded, enter = fadeIn(), exit = fadeOut(), ) { when (state) { - is OnrampV2AmountButtonUMState.Loaded -> { + is OnrampAmountButtonUMState.Loaded -> { if (keyboard is Keyboard.Opened) { LazyRow( modifier = Modifier.background(color = TangemTheme.colors.button.secondary), @@ -82,7 +82,7 @@ private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { } } } - OnrampV2AmountButtonUMState.None -> Unit + OnrampAmountButtonUMState.None -> Unit } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index fa0797f476..2b24ad2fb9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -5,13 +5,12 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import com.tangem.core.ui.components.CircleShimmer +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification @@ -21,41 +20,58 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.main.entity.OnrampMainComponentUM @Composable -internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { +internal fun OnrampMainScreen(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { Scaffold( - modifier = modifier.imePadding(), - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, + modifier = modifier.systemBarsPadding(), topBar = { TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), startButton = state.topBarConfig.startButtonUM, endButton = state.topBarConfig.endButtonUM, title = state.topBarConfig.title.resolveReference(), ) }, - content = { innerPadding -> - val contentModifier = Modifier - .padding(innerPadding) - .padding(horizontal = TangemTheme.dimens.spacing16) + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + OnrampMainComponentContent( + state = state, + modifier = Modifier.padding(scaffoldPaddings), + ) + } +} + +@Composable +internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + Column( + modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { when (state) { - is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state) - is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state) + is OnrampMainComponentUM.InitialLoading -> InitialLoading(state = state) + is OnrampMainComponentUM.Content -> Content(state = state) } - }, - floatingActionButton = { - OnrampButtonComponent(state) - }, - floatingActionButtonPosition = FabPosition.Center, - ) + } + + if (state is OnrampMainComponentUM.Content) { + OnrampFooterContent(state = state) + } + } } @Composable private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) { Column( - modifier = modifier, + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() @@ -64,27 +80,38 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier } @Composable -private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { +private fun OnrampAmountContentLoading() { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) .background(TangemTheme.colors.background.action) .padding(vertical = TangemTheme.dimens.spacing28), horizontalAlignment = Alignment.CenterHorizontally, ) { - CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40)) RectangleShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24), - radius = TangemTheme.dimens.radius3, + .size(width = 76.dp, height = 20.dp), + radius = TangemTheme.dimens.radius4, ) RectangleShimmer( modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12), - radius = TangemTheme.dimens.radius3, + .padding(top = TangemTheme.dimens.spacing12) + .size(width = 136.dp, height = 44.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(width = 52.dp, height = 16.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing20) + .size(width = 84.dp, height = 28.dp), + radius = TangemTheme.dimens.radius14, ) } } @@ -93,13 +120,20 @@ private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .wrapContentHeight() .navigationBarsPadding() - .padding(bottom = TangemTheme.dimens.spacing76), + .padding( + bottom = 76.dp, + start = 16.dp, + end = 16.dp, + ), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - OnrampAmountContent(state = state.amountBlockState) - OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth()) + OnrampAmountContent(state = state) + + OnrampOffersContent(state = state.offersBlockState) + if (state.errorNotification != null) Notification(config = state.errorNotification.config) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt similarity index 99% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt index dba9eac25a..4c161835e9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility @@ -33,7 +33,7 @@ import com.tangem.core.ui.test.OnrampOffersBlockTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import kotlinx.collections.immutable.persistentListOf @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt deleted file mode 100644 index e13fd96310..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.onramp.main.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import com.tangem.core.ui.extensions.appendSpace -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon - -@Composable -internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) { - when (state) { - is OnrampProviderBlockUM.Empty -> Unit - is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier) - is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state) - } -} - -@Composable -private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .clickable(onClick = state.onClick) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - PaymentMethodIcon(imageUrl = state.paymentMethod.imageUrl) - Column(modifier = Modifier.weight(1F)) { - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_pay_with)) - appendSpace() - withStyle( - style = SpanStyle( - fontWeight = TangemTheme.typography.subtitle2.fontWeight, - color = TangemTheme.colors.text.primary1, - ), - ) { - append(state.paymentMethod.name) - } - }, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_via)) - appendSpace() - append(state.providerName) - }, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - AnimatedVisibility( - visible = state.isBestRate, - enter = fadeIn(), - exit = fadeOut(), - label = "Best Rate visibility animation", - ) { - Text( - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent, - shape = RoundedCornerShape(TangemTheme.dimens.radius4), - ) - .padding( - horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing1, - ), - text = stringResourceSafe(id = R.string.express_provider_best_rate), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary2, - ) - } - } -} - -@Composable -private fun OnrampProviderLoading(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - text = stringResourceSafe(id = R.string.express_provider), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - strokeWidth = TangemTheme.dimens.size2, - modifier = Modifier.size(TangemTheme.dimens.size16), - ) - Text( - text = stringResourceSafe(id = R.string.express_fetch_best_rates), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt deleted file mode 100644 index 4d417ad448..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.onramp.alloffers.AllOffersComponent -import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen -import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultOnrampV2MainComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: OnrampV2MainComponent.Params, - private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, - private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val allOffersComponentFactory: AllOffersComponent.Factory, -) : OnrampV2MainComponent, AppComponentContext by appComponentContext { - - private val model: OnrampV2MainComponentModel = getOrCreateModel(params) - - init { - lifecycle.subscribe(onStop = model::onStop) - } - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = null, - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsState() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - OnrampNewMainScreen(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: OnrampV2MainBottomSheetConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( - context = childByContext(componentContext), - params = ConfirmResidencyComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - country = config.country, - isLaunchSepa = false, - onDismiss = { - model.bottomSheetNavigation.dismiss() - model.handleOnrampAvailable() - }, - ), - ) - is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( - context = childByContext(componentContext), - params = SelectCurrencyComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( - context = childByContext(componentContext), - params = AllOffersComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - openRedirectPage = params.openRedirectPage, - amountCurrencyCode = config.amountCurrencyCode, - ), - ) - } - - @AssistedFactory - interface Factory : OnrampV2MainComponent.Factory { - override fun create( - context: AppComponentContext, - params: OnrampV2MainComponent.Params, - ): DefaultOnrampV2MainComponent - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt deleted file mode 100644 index 815fa5060b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -class DefaultOnrampV2MainFeatureToggle( - private val featureTogglesManager: FeatureTogglesManager, -) : OnrampV2MainFeatureToggle { - override val isOnrampNewMainEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED") -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt deleted file mode 100644 index 9767cf4496..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampSource - -internal interface OnrampV2MainComponent : ComposableContentComponent { - - data class Params( - val userWalletId: UserWalletId, - val cryptoCurrency: CryptoCurrency, - val source: OnrampSource, - val openSettings: () -> Unit, - val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt deleted file mode 100644 index 54595ff8d7..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -internal interface OnrampV2MainFeatureToggle { - val isOnrampNewMainEnabled: Boolean -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt deleted file mode 100644 index 84fb039ffd..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainV2ComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampV2MainComponentModel::class) - fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt deleted file mode 100644 index 08817d31ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface OnrampNewMainComponentModule { - - @Binds - @Singleton - fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory -} - -@Module -@InstallIn(SingletonComponent::class) -internal object FeatureToggleModule { - - @Provides - @Singleton - fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle { - return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt deleted file mode 100644 index 49041e269a..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampProviderWithQuote - -internal interface OnrampV2Intents { - fun onAmountValueChanged(value: String) - fun openSettings() - fun openCurrenciesList() - fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) - fun openProviders() - fun onRefresh() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt deleted file mode 100644 index afc654a422..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampCountry -import kotlinx.serialization.Serializable - -@Serializable -sealed interface OnrampV2MainBottomSheetConfig { - @Serializable - data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig - - @Serializable - data object CurrenciesList : OnrampV2MainBottomSheetConfig - - @Serializable - data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt deleted file mode 100644 index 256aeadd72..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed interface OnrampV2MainComponentUM { - - val topBarConfig: OnrampV2MainTopBarUM - val errorNotification: NotificationUM? - - data class InitialLoading( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - ) : OnrampV2MainComponentUM - - data class Content( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - val amountBlockState: OnrampNewAmountBlockUM, - val offersBlockState: OnrampOffersBlockUM, - val onrampAmountButtonUMState: OnrampV2AmountButtonUMState, - ) : OnrampV2MainComponentUM -} - -internal data class OnrampV2MainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt deleted file mode 100644 index 750e9ba8f3..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed interface OnrampV2ProvidersUM { - - data object Empty : OnrampV2ProvidersUM - - data object Loading : OnrampV2ProvidersUM - - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - ) : OnrampV2ProvidersUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt deleted file mode 100644 index 06ec867696..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity.factory - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.Amount -import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.utils.Provider -import java.math.BigDecimal - -internal class OnrampV2StateFactory( - private val currentStateProvider: Provider, - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val cryptoCurrency: CryptoCurrency, - private val onrampIntents: OnrampV2Intents, -) { - - fun getInitialState( - currency: String, - onClose: () -> Unit, - openSettings: () -> Unit, - ): OnrampV2MainComponentUM.InitialLoading { - return OnrampV2MainComponentUM.InitialLoading( - errorNotification = null, - topBarConfig = OnrampV2MainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Close( - onCloseClick = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ), - ) - } - - fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content { - val state = currentStateProvider() - - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - val initialAmountBlockState = getInitialAmountBlockState(currency) - - return OnrampV2MainComponentUM.Content( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - amountBlockState = initialAmountBlockState, - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = null, - onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( - currencyCode = currency.code, - currencySymbol = currency.unit, - onAmountValueChanged = onrampIntents::onAmountValueChanged, - ), - ) - } - - fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM { - return when (onrampError) { - OnrampError.PairsNotFound -> getNoPairsErrorState() - is OnrampError.DataError -> getErrorState( - errorCode = onrampError.code, - onRefresh = onrampIntents::onRefresh, - ) - is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh) - is OnrampError.AmountError.TooBigError, - is OnrampError.AmountError.TooSmallError, - OnrampError.RedirectError.VerificationFailed, - OnrampError.RedirectError.WrongRequestId, - OnrampError.AlreadyHandledTransaction, - -> currentStateProvider() // ignore error state - } - } - - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM { - val state = currentStateProvider() - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - return when (state) { - is OnrampV2MainComponentUM.Content -> state.copy( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) - is OnrampV2MainComponentUM.InitialLoading -> state.copy( - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - ) - } - } - - private fun getNoPairsErrorState(): OnrampV2MainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampV2MainComponentUM.Content ?: return state - - return contentState.copy( - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - offersBlockState = OnrampOffersBlockUM.Empty, - ) - } - - private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM { - return OnrampNewAmountBlockUM( - currencyUM = OnrampNewCurrencyUM( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - onClick = onrampIntents::openCurrenciesList, - unit = currency.unit, - ), - amountFieldModel = AmountFieldModel( - value = "", - fiatValue = "", - onValueChange = onrampIntents::onAmountValueChanged, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions(), - isFiatValue = true, - cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), - fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), - isError = false, - isWarning = false, - error = TextReference.EMPTY, - isFiatUnavailable = false, - isValuePasted = false, - onValuePastedTriggerDismiss = {}, - ), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ) - } - - private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( - currencySymbol = currency.unit, - value = this, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt deleted file mode 100644 index 517c304fd6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ /dev/null @@ -1,417 +0,0 @@ -package com.tangem.features.onramp.mainv2.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.ui.components.fields.InputManager -import com.tangem.domain.onramp.* -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampOffersStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory -import com.tangem.features.onramp.utils.sendOnrampErrorEvent -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.PeriodicTask -import com.tangem.utils.coroutines.SingleTaskScheduler -import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList", "LargeClass") -internal class OnrampV2MainComponentModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val router: Router, - private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, - private val getOnrampCountryUseCase: GetOnrampCountryUseCase, - private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, - private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, - private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, - private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val amountInputManager: InputManager, - private val getOnrampOffersUseCase: GetOnrampOffersUseCase, - paramsContainer: ParamsContainer, - getWalletsUseCase: GetWalletsUseCase, -) : Model(), OnrampV2Intents { - - val params = paramsContainer.require() - - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampAmountButtonUMStateFactory() - } - - @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2StateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - val state: StateFlow - field = MutableStateFlow( - value = stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = ::onCloseClick, - openSettings = ::openSettings, - ), - ) - - private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2AmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampOffersStateFactory( - currentStateProvider = Provider { state.value }, - onrampIntents = this, - ) - } - - private val quotesTaskScheduler = SingleTaskScheduler() - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - init { - modelScope.launch { - clearOnrampCacheUseCase() - } - startLoadingQuotes() - sendScreenOpenAnalytics() - checkResidenceCountry() - subscribeToAmountChanges() - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - subscribeOnOffers() - } - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - super.onDestroy() - } - - override fun onAmountValueChanged(value: String) { - state.update { amountStateFactory.getOnAmountValueChange(value) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings.invoke() - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) - } - - override fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - sendOfferClickEvent( - quote = quote, - onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, - categoryUM = categoryUM, - ) - params.openRedirectPage(quote) - } - - override fun openProviders() { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode)) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - openSettings = ::openSettings, - ) - } - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - handleOnrampAvailable() - } - } - - fun onStop() { - quotesTaskScheduler.cancelTask() - } - - fun handleOnrampAvailable() { - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - } - - private fun startLoadingQuotes() { - quotesTaskScheduler.cancelTask() - quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) - } - - private fun loadQuotesTask(): PeriodicTask { - return PeriodicTask( - delay = UPDATE_DELAY, - task = { - runSuspendCatching { - val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState - ?: return@runSuspendCatching - - val fiatAmount = amountBlockState.amountFieldModel.fiatAmount - if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching - - fetchQuotesUseCase.invoke( - userWallet = userWallet, - amount = amountBlockState.amountFieldModel.fiatAmount, - cryptoCurrency = params.cryptoCurrency, - ).onLeft(::handleOnrampError) - } - }, - onSuccess = {}, - onError = {}, - ) - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> Unit - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - - private fun subscribeOnOffers() = modelScope.launch { - getOnrampOffersUseCase - .invoke() - .collectLatest { maybeOffers -> - maybeOffers.fold( - ifLeft = ::handleOnrampError, - ifRight = { offers -> - val currentState = state.value - if (currentState is OnrampV2MainComponentUM.Content) { - if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { - state.update { - currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } - return@fold - } - state.update { - onrampOffersStateFactory.getOffersState(offers) - } - } - }, - ) - } - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - startLoadingQuotes() - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - state.update { prevState -> - when (prevState) { - is OnrampV2MainComponentUM.Content -> { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - is OnrampV2MainComponentUM.InitialLoading -> { - stateFactory.getReadyState(country.defaultCurrency) - } - } - } - updatePairsAndQuotes() - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToQuotesUpdate() { - getOnrampQuotesUseCase.invoke() - .conflate() - .onEach { maybeQuotes -> - maybeQuotes.fold( - ifLeft = ::handleOnrampError, - ifRight = ::handleQuoteResult, - ) - } - .launchIn(modelScope) - } - - private fun handleQuoteResult(quotes: List) { - sendOnrampQuotesErrorAnalytic(quotes) - when { - quotes.isEmpty() -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - quotes.all { it is OnrampQuote.AmountError } -> { - state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } - } - quotes.none { it is OnrampQuote.Data } -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - else -> { - state.update { prevState -> - val resetState = amountStateFactory.getAmountSecondaryFieldResetState() - if (prevState is OnrampV2MainComponentUM.Content && - resetState is OnrampV2MainComponentUM.Content && - prevState.offersBlockState is OnrampOffersBlockUM.Loading - ) { - resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } else { - resetState - } - } - } - } - } - - private fun onRetryQuotes() { - state.update { prevState -> - (prevState as? OnrampV2MainComponentUM.Content)?.copy( - errorNotification = null, - offersBlockState = OnrampOffersBlockUM.Loading, - amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) ?: prevState - } - startLoadingQuotes() - } - - private suspend fun updatePairsAndQuotes() { - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { - state.update { - amountStateFactory.getAmountSecondaryFieldResetState() - } - startLoadingQuotes() - }, - ) - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - - private fun sendOnrampQuotesErrorAnalytic(quotes: List) { - quotes.forEach { errorState -> - when (errorState) { - is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - else -> Unit - } - } - } - - private fun sendScreenOpenAnalytics() { - analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - } - - private fun sendOfferClickEvent( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val event = when (categoryUM) { - OnrampOfferCategoryUM.RecentlyUsed -> { - OnrampAnalyticsEvent.RecentlyBuyClicked( - tokenSymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethod = quote.paymentMethod.name, - ) - } - OnrampOfferCategoryUM.Recommended -> { - onrampOfferAdvantagesUM.toAnalyticsEvent( - cryptoCurrencySymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethodName = quote.paymentMethod.name, - ) - } - } - - if (event != null) { - analyticsEventHandler.send(event) - } - } - - private companion object { - const val UPDATE_DELAY = 10_000L - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt deleted file mode 100644 index 7e9499e37d..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -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.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Scaffold( - modifier = modifier.systemBarsPadding(), - topBar = { - TangemTopAppBar( - startButton = state.topBarConfig.startButtonUM, - endButton = state.topBarConfig.endButtonUM, - title = state.topBarConfig.title.resolveReference(), - ) - }, - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, - ) { scaffoldPaddings -> - OnrampNewMainComponentContent( - state = state, - modifier = Modifier.padding(scaffoldPaddings), - ) - } -} - -@Composable -internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.secondary), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - when (state) { - is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state) - is OnrampV2MainComponentUM.Content -> Content(state = state) - } - } - - if (state is OnrampV2MainComponentUM.Content) { - OnrampFooterContent(state = state) - } - } -} - -@Composable -private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampAmountContentLoading() - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} - -@Composable -private fun OnrampAmountContentLoading() { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = 76.dp, height = 20.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12) - .size(width = 136.dp, height = 44.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing8) - .size(width = 52.dp, height = 16.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing20) - .size(width = 84.dp, height = 28.dp), - radius = TangemTheme.dimens.radius14, - ) - } -} - -@Composable -private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .navigationBarsPadding() - .padding( - bottom = 76.dp, - start = 16.dp, - end = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampV2AmountContent(state = state) - - OnrampOffersContent(state = state.offersBlockState) - - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt deleted file mode 100644 index 8cc5c7aca6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.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.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags -import com.tangem.core.ui.utils.rememberDecimalFormat -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM -import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), - ) - .padding(vertical = 24.dp, horizontal = 16.dp) - .animateContentSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnrampHeaderTitle() - - OnrampAmountField( - amountField = state.amountBlockState.amountFieldModel, - currencyCode = state.amountBlockState.currencyUM.code, - ) - - AnimatedVisibility( - visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, - ) { - if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { - OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) - } - } - - SpacerH(20.dp) - - OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) - } -} - -@Composable -private fun OnrampHeaderTitle() { - Text( - text = stringResourceSafe(R.string.onramp_you_will_pay_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { - val decimalFormat = rememberDecimalFormat() - val requester = remember { FocusRequester() } - AmountTextField( - value = amountField.fiatValue, - decimals = amountField.fiatAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = amountField.fiatAmount.decimals, - symbol = currencyCode, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - }, - ), - onValueChange = amountField.onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.head.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isEnabled = !amountField.isError, - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing4, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), - ) - - LaunchedEffect(key1 = Unit) { - requester.requestFocus() - } -} - -@Composable -private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - contentAlignment = Alignment.Center, - ) { - Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(RoundedCornerShape(14.dp)) - .background(TangemTheme.colors.button.secondary) - .clickable(onClick = currencyUM.onClick) - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - AsyncImage( - modifier = Modifier - .size(20.dp) - .clip(CircleShape) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), - model = currencyUM.iconUrl, - contentDescription = null, - ) - Text( - text = currencyUM.code, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), - textAlign = TextAlign.Center, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 1133a72cfc..daa379e850 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -17,8 +17,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent @@ -32,9 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, - private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory, private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, - private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -71,44 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor( onBack = navigation::pop, ), ) - OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) { - onrampMainV2ComponentFactory.create( - context = childByContext(componentContext), - params = OnrampV2MainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { quote -> - navigation.push( - OnrampChild.RedirectPage( - quote = quote, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - ), - ) - } else { - onrampMainComponentFactory.create( - context = childByContext(componentContext), - params = OnrampMainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { onrampProviderWithQuoteData -> - navigation.push( - OnrampChild.RedirectPage( - quote = onrampProviderWithQuoteData, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - isLaunchSepa = params.shouldLaunchSepa, - ), - ) - } + OnrampChild.Main -> onrampMainComponentFactory.create( + context = childByContext(componentContext), + params = OnrampMainComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + openSettings = { navigation.push(OnrampChild.Settings) }, + source = params.source, + openRedirectPage = { quote -> + navigation.push( + OnrampChild.RedirectPage( + quote = quote, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( context = childByContext(componentContext), params = OnrampRedirectComponent.Params( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 1c618de087..a9ac694ab9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -3,25 +3,25 @@ package com.tangem.features.onramp.selecttoken.model import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM 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.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R @@ -43,7 +43,8 @@ internal class OnrampOperationModel @Inject constructor( private val router: AppRouter, private val analyticsEventHandler: AnalyticsEventHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val rampStateManager: RampStateManager, @@ -119,9 +120,13 @@ internal class OnrampOperationModel @Inject constructor( val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync() .getOrElse { AppCurrency.Default }.code - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell(status, appCurrencyCode), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = appCurrencyCode, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } @@ -153,18 +158,9 @@ internal class OnrampOperationModel @Inject constructor( private fun showErrorIfDemoModeOrElse(action: () -> Unit) { if (selectedUserWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) - }, - secondActionBuilder = { cancelAction() }, + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), ) messageSender.send(message) 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 8040f59244..61552e4822 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 @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TotalFiatBalance @@ -15,7 +16,7 @@ internal class LoadingAccountTokenItemConverter( override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio { val (account, currencies) = value - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, @@ -26,6 +27,6 @@ internal class LoadingAccountTokenItemConverter( tokens = currencies.flattenCurrencies() .map { LoadingTokenListItemConverter.convert(it.currency) } .toPersistentList(), - ) + ).convert(Unit) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt index d46f06637e..08f93f0bd0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -1,34 +1,64 @@ package com.tangem.features.onramp.swap.availablepairs.entity.transformers +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory -import com.tangem.features.onramp.tokenlist.entity.utils.addHeader import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList -/** -[REDACTED_AUTHOR] - */ internal class SetNoAvailablePairsTransformer( private val appCurrency: AppCurrency, - private val unavailableStatuses: List, + private val accountList: Map>, private val isBalanceHidden: Boolean, - private val unavailableTokensHeaderReference: TextReference, + private val isAccountsMode: Boolean, + private val unavailableErrorText: TextReference, ) : TokenListUMTransformer { + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { - val unavailableItems = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency) - .convertList(unavailableStatuses) - .map(TokensListItemUM::Token) + val totalTokensCount = accountList.values.sumOf { it.size } return prevState.copy( availableItems = persistentListOf(), - unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountList.map { (account, cryptoCurrencies) -> + TokensListPortfolioItemConverter( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + .toPersistentList(), + ).convert(Unit) + }.toPersistentList(), + totalTokensCount = totalTokensCount, + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, cryptoCurrencies) -> + unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + }.toPersistentList(), + totalTokensCount = totalTokensCount, + ) + }, isBalanceHidden = isBalanceHidden, warning = NotificationUM.Warning.SwapNoAvailablePair, ) 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 deleted file mode 100644 index ac87e348b8..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.features.onramp.swap.availablepairs.entity.transformers - -import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.entity.TokenListUMData -import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList - -internal class SetNoAvailablePairsTransformerV2( - private val appCurrency: AppCurrency, - private val accountList: Map>, - private val isBalanceHidden: Boolean, - private val isAccountsMode: Boolean, - private val unavailableErrorText: TextReference, -) : TokenListUMTransformer { - private val unavailableConverter = OnrampTokenItemStateConverterFactory - .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(), - tokensListData = if (isAccountsMode) { - TokenListUMData.AccountList( - tokensList = accountList.map { (account, cryptoCurrencies) -> - TokensListItemUM.Portfolio( - tokenItemUM = AccountCryptoPortfolioItemStateConverter( - appCurrency = appCurrency, - account = account, - onItemClick = null, - ).convert(TotalFiatBalance.Failed), - isExpanded = true, - isCollapsable = false, - tokens = unavailableConverter.convertList(cryptoCurrencies) - .map(TokensListItemUM::Token) - .toPersistentList(), - ) - }.toPersistentList(), - totalTokensCount = totalTokensCount, - ) - } else { - TokenListUMData.TokenList( - tokensList = accountList.flatMap { (_, cryptoCurrencies) -> - unavailableConverter.convertList(cryptoCurrencies) - .map(TokensListItemUM::Token) - }.toPersistentList(), - totalTokensCount = totalTokensCount, - ) - }, - isBalanceHidden = isBalanceHidden, - warning = NotificationUM.Warning.SwapNoAvailablePair, - ) - } -} \ 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 363f0712d5..f3b60a2dcd 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 @@ -13,10 +13,7 @@ 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 @@ -26,7 +23,6 @@ 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 @@ -41,7 +37,6 @@ 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 @@ -51,9 +46,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor 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 @@ -61,8 +54,11 @@ 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.transformer.SetLoadingAccountTokenListTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -86,7 +82,6 @@ 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, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -94,7 +89,6 @@ internal class AvailableSwapPairsModel @Inject constructor( private val getAvailablePairsUseCase: GetAvailablePairsUseCase, 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, @@ -119,7 +113,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private val addToPortfolioJobHolder = JobHolder() - private val tokenListFlow = getTokenListUseCaseFlow() private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) @@ -156,13 +149,10 @@ internal class AvailableSwapPairsModel @Inject constructor( private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) init { - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeOnUpdateStateV2() - } else { - subscribeOnUpdateState() - } + subscribeOnUpdateState() initializeSearchBarCallbacks() + subscribeOnSelectedStatusChange() subscribeOnAvailablePairsUpdates() if (swapFeatureToggles.isMarketListFeatureEnabled) { @@ -171,18 +161,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } } - private fun getTokenListUseCaseFlow(): SharedFlow> { - return getTokenListUseCase.launch(userWalletId = params.userWalletId) - .distinctUntilChanged() - .map { maybeTokenList -> - maybeTokenList.getOrElse( - ifLoading = { it ?: TokenList.Empty }, - ifError = { TokenList.Empty }, - ).flattenCurrencies() - } - .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) - } - private fun getAccountListUseCaseFlow(): SharedFlow> { return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId)) .distinctUntilChanged() @@ -195,6 +173,13 @@ internal class AvailableSwapPairsModel @Inject constructor( .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) } + private fun subscribeOnSelectedStatusChange() { + params.selectedStatus + .filter { it == null } + .onEach { clearSearchState() } + .launchIn(modelScope) + } + private fun initializeSearchBarCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( @@ -205,42 +190,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun subscribeOnUpdateState() { - combine( - flow = tokenListFlow, - flow2 = getAppCurrencyAndBalanceHidingFlow(), - flow3 = params.selectedStatus, - flow4 = searchManager.query, - flow5 = availablePairsByNetworkFlow - .map { it[params.selectedStatus.value?.toLeastTokenInfo()] } - .distinctUntilChanged(), - ) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState -> - availablePairsState?.fold( - ifLoading = { SetLoadingTokenItemsTransformer(currencies) }, - ifContent = { pairs -> - handleContentState( - appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, - currencies = currencies, - selectedStatus = selectedStatus, - query = query, - availablePairs = pairs, - ) - }, - ifError = { throwable -> - handleErrorState( - cause = throwable, - networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), - currencies = currencies, - ) - }, - ) - ?: SetLoadingTokenItemsTransformer(currencies) - } - .onEach(tokenListUMController::update) - .flowOn(dispatchers.main) - .launchIn(modelScope) - } - - private fun subscribeOnUpdateStateV2() { combine( flow = getAccountsAndModeFlow(), flow2 = getAppCurrencyAndBalanceHidingFlow(), @@ -260,7 +209,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ) }, ifContent = { pairs -> - handleContentStateV2( + handleContentState( appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, accountList = accountList, selectedStatus = selectedStatus, @@ -270,7 +219,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ) }, ifError = { throwable -> - handleErrorStateV2( + handleErrorState( cause = throwable, networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), accountList = accountList, @@ -288,52 +237,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun handleContentState( - appCurrencyAndBalanceHiding: Pair, - currencies: List, - selectedStatus: CryptoCurrencyStatus?, - query: String, - availablePairs: List, - ): TokenListUMTransformer { - val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding - - if (availablePairs.isEmpty()) { - return SetNoAvailablePairsTransformer( - appCurrency = appCurrency, - unavailableStatuses = currencies, - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = resourceReference( - id = R.string.tokens_list_unavailable_to_swap_header, - wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()), - ), - ) - } - - val filterByQueryTokenList = currencies - .filter { it.currency != selectedStatus?.currency } - .filterByQuery(query = query) - - return if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { - SetNothingToFoundStateTransformer( - isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = resourceReference( - id = R.string.action_buttons_swap_empty_search_message, - ), - ) - } else { - UpdateTokenItemsTransformer( - appCurrency = appCurrency, - onItemClick = ::onPortfolioTokenClick, - statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = resourceReference( - id = R.string.tokens_list_unavailable_to_swap_header, - wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()), - ), - ) - } - } - - private fun handleContentStateV2( appCurrencyAndBalanceHiding: Pair, accountList: List, selectedStatus: CryptoCurrencyStatus?, @@ -362,7 +265,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .filterValues { it.isNotEmpty() } if (availablePairs.isEmpty()) { - return SetNoAvailablePairsTransformerV2( + return SetNoAvailablePairsTransformer( appCurrency = appCurrency, accountList = filterByQueryAccountList, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -372,7 +275,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { - SetNothingToFoundStateTransformerV2( + SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, emptySearchMessageReference = resourceReference( id = R.string.action_buttons_swap_empty_search_message, @@ -391,23 +294,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun handleErrorState( - cause: Throwable, - networkInfo: LeastTokenInfo?, - currencies: List, - ): SetErrorWarningTransformer { - return SetErrorWarningTransformer( - cause = cause, - onRefresh = { - modelScope.launch { - if (networkInfo != null) { - updateAvailablePairs(networkInfo, currencies) - } - } - }, - ) - } - - private fun handleErrorStateV2( cause: Throwable, networkInfo: LeastTokenInfo?, accountList: List, @@ -441,19 +327,14 @@ internal class AvailableSwapPairsModel @Inject constructor( val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest - if (accountsFeatureToggles.isFeatureEnabled) { - val accountList = accountListFlow.firstOrNull() ?: return@collectLatest - updateAvailablePairs( - networkInfo = networkInfo, - statuses = accountList.filterCryptoPortfolio() - .flatMap { accountStatus -> - accountStatus.flattenCurrencies() - }.toSet().toList(), - ) - } else { - val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest - updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) - } + val accountList = accountListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs( + networkInfo = networkInfo, + statuses = accountList.filterCryptoPortfolio() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + }.toSet().toList(), + ) } } } @@ -531,19 +412,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } } - private fun List.filterByAvailability( - availablePairs: List, - ): Map> { - return groupBy { status -> - val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo()) - - isAvailable && - status.value !is CryptoCurrencyStatus.MissedDerivation && - status.value !is CryptoCurrencyStatus.Unreachable && - !status.currency.isCustom - } - } - private fun Map>.filterByAvailability( availablePairs: List, ): List { @@ -575,9 +443,22 @@ internal class AvailableSwapPairsModel @Inject constructor( isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) + clearSearchState() params.onTokenClick(tokenItem, status) } + private fun clearSearchState() { + tokenListUMController.update( + transformer = ClearSearchBarTransformer( + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + modelScope.launch { + searchManager.update("") + } + searchQueryStateForMarkets.value = "" + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", @@ -695,6 +576,8 @@ internal class AvailableSwapPairsModel @Inject constructor( ), ) + clearSearchState() + // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) refreshPairsTrigger.emit(Unit) @@ -725,6 +608,8 @@ internal class AvailableSwapPairsModel @Inject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index 49a9b6d86d..dcf23013a0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -8,7 +8,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -37,7 +36,6 @@ internal class SwapSelectTokensModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model() { val state: StateFlow = controller.state @@ -77,14 +75,10 @@ internal class SwapSelectTokensModel @Inject constructor( selectedTokenItemState = selectedTokenItemState, onRemoveClick = ::onRemoveFromTokenClick, isAccountsMode = isAccountsMode, - account = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = params.userWalletId, - currency = status.currency, - ).getOrNull()?.account - } else { - null - }, + account = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = params.userWalletId, + currency = status.currency, + ).getOrNull()?.account, ), ) } @@ -108,14 +102,10 @@ internal class SwapSelectTokensModel @Inject constructor( transformer = SelectToTokenTransformer( selectedTokenItemState = selectedTokenItemState, isAccountsMode = isAccountsMode, - account = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = params.userWalletId, - currency = status.currency, - ).getOrNull()?.account - } else { - null - }, + account = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = params.userWalletId, + currency = status.currency, + ).getOrNull()?.account, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt index 1b388e9a47..ad495bf4ae 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt @@ -2,8 +2,6 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer @@ -16,22 +14,18 @@ internal class SetNothingToFoundStateTransformer( override fun transform(prevState: TokenListUM): TokenListUM { return prevState.copy( - availableItems = persistentListOf( - createGroupTitle( - textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header), - ), - TokensListItemUM.Text( - id = emptySearchMessageReference.hashCode(), - text = emptySearchMessageReference, - ), - ), + availableItems = persistentListOf(), unavailableItems = persistentListOf(), - tokensListData = TokenListUMData.EmptyList, + tokensListData = TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.Text( + id = emptySearchMessageReference.hashCode(), + text = emptySearchMessageReference, + ), + ), + totalTokensCount = 0, + ), isBalanceHidden = isBalanceHidden, ) } - - private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle { - return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference) - } } \ No newline at end of file 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 deleted file mode 100644 index 422e445dff..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.features.onramp.tokenlist.entity.transformer - -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.entity.TokenListUMData -import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import kotlinx.collections.immutable.persistentListOf - -internal class SetNothingToFoundStateTransformerV2( - private val isBalanceHidden: Boolean, - private val emptySearchMessageReference: TextReference, -) : TokenListUMTransformer { - - override fun transform(prevState: TokenListUM): TokenListUM { - return prevState.copy( - availableItems = persistentListOf(), - unavailableItems = persistentListOf(), - tokensListData = TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.Text( - id = emptySearchMessageReference.hashCode(), - text = emptySearchMessageReference, - ), - ), - totalTokensCount = 0, - ), - isBalanceHidden = isBalanceHidden, - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt index f90f1e9794..791645af20 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter 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 @@ -25,7 +26,7 @@ internal class UpdateAccountTokenItemConverter( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio { - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, @@ -40,6 +41,6 @@ internal class UpdateAccountTokenItemConverter( unavailableConverter.convert(status) } }.map(TokensListItemUM::Token).toPersistentList(), - ) + ).convert(Unit) } } \ No newline at end of file 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 a90f9e6e25..ddc4c9751a 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 @@ -5,9 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM 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.resourceReference -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier @@ -15,19 +15,14 @@ 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.core.lce.Lce -import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetAssetRequirementsUseCase -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R @@ -35,7 +30,10 @@ import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent import com.tangem.features.onramp.tokenlist.entity.* -import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer +import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -58,12 +56,10 @@ internal class OnrampTokenListModel @Inject constructor( private val searchManager: InputManager, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getTokenListUseCase: GetTokenListUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val rampStateManager: RampStateManager, private val getUserCountryUseCase: GetUserCountryUseCase, private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, ) : Model() { @@ -82,68 +78,10 @@ internal class OnrampTokenListModel @Inject constructor( onActiveChange = ::onSearchBarActiveChange, ), ) - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeOnUpdateStateV2() - } else { - subscribeOnUpdateState() - } + subscribeOnUpdateState() } private fun subscribeOnUpdateState() { - combine( - flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), - flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), - flow4 = searchManager.query, - flow5 = hasRestrictionForSellFlow(), - ) { maybeTokenList, appCurrency, isBalanceHidden, query, hasRestrictionForSell -> - val currencies = maybeTokenList.getOrElse( - ifLoading = { it ?: TokenList.Empty }, - ifError = { TokenList.Empty }, - ) - .flattenCurrencies() - - val filterByQueryTokenList = currencies - .filterByQuery(query = query) - - if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { - SetNothingToFoundStateTransformer( - isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = getEmptySearchMessageReference(), - ) - } else { - val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) { - maybeTokenList.isInsufficientBalanceForSell() - } else { - false - } - - UpdateTokenItemsTransformer( - appCurrency = appCurrency, - onItemClick = params.onTokenClick, - statuses = filterByQueryTokenList.let { statuses -> - if (hasRestrictionForSell || isInsufficientBalanceForSell) { - mapOf(false to statuses) - } else { - statuses.filterByAvailability() - } - }, - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = getUnavailableTokensHeaderReference(), - warning = when { - hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction - isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling - else -> null - }, - ) - } - } - .onEach(::updateTokenListUM) - .flowOn(dispatchers.main) - .launchIn(modelScope) - } - - private fun subscribeOnUpdateStateV2() { combine( flow = singleAccountStatusListSupplier( SingleAccountStatusListProducer.Params(params.userWalletId), @@ -158,7 +96,7 @@ internal class OnrampTokenListModel @Inject constructor( if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { updateTokenListUM( - SetNothingToFoundStateTransformerV2( + SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, emptySearchMessageReference = getEmptySearchMessageReference(), ), @@ -174,7 +112,7 @@ internal class OnrampTokenListModel @Inject constructor( updateTokenListUM( UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onTokenClick, accountList = filterByQueryAccountList.filterByAvailability(), isBalanceHidden = isBalanceHidden, unavailableErrorText = getUnavailableTokensHeaderReference(), @@ -209,16 +147,6 @@ internal class OnrampTokenListModel @Inject constructor( } } - private fun Lce.isInsufficientBalanceForSell(): Boolean { - return if (params.filterOperation == OnrampOperation.SELL) { - isContent { - (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true - } - } else { - false - } - } - private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean { return if (params.filterOperation == OnrampOperation.SELL) { (totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true @@ -266,12 +194,23 @@ internal class OnrampTokenListModel @Inject constructor( } private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean { - return if (accountsFeatureToggles.isFeatureEnabled) { - prevState.tokensListData == TokenListUMData.EmptyList && - newState.tokensListData != TokenListUMData.EmptyList - } else { - prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() && - (newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty()) + return prevState.tokensListData == TokenListUMData.EmptyList && + newState.tokensListData != TokenListUMData.EmptyList + } + + private fun onTokenClick(tokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + clearSearchState() + params.onTokenClick(tokenItemState, status) + } + + private fun clearSearchState() { + tokenListUMController.update( + transformer = ClearSearchBarTransformer( + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + modelScope.launch { + searchManager.update("") } } @@ -315,41 +254,6 @@ internal class OnrampTokenListModel @Inject constructor( } } - private suspend fun List.filterByAvailability(): Map> { - return coroutineScope { - map { status -> - async { - val isOperationAvailable = checkAvailabilityByOperation(status = status) - val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation - val isNotLoading = status.value !is CryptoCurrencyStatus.Loading - - val requirements = getAssetRequirementsUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - ).getOrNull() - - val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements) - val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable - - val isAvailable = when (params.filterOperation) { - OnrampOperation.BUY -> { - isAvailableForBuy - } // unreachable state is available for Buy operation - OnrampOperation.SELL -> isNotUnreachable - OnrampOperation.SWAP -> { - isNotUnreachable && isAvailableForBuy - } - } - - status to (isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable) - } - } - .awaitAll() - .groupBy(Pair::second) - .mapValues { it.value.map(Pair::first) } - } - } - private suspend fun AccountCryptoList.filterByAvailability(): List { return coroutineScope { map { (account, currencies) -> diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt new file mode 100644 index 0000000000..e38f56cf73 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference + +internal class ClearSearchBarTransformer( + private val placeHolder: TextReference, +) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy( + query = "", + isActive = false, + placeholderText = placeHolder, + ) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt index 67af26dc2a..49ad108455 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -1,6 +1,6 @@ package com.tangem.feature.referral.domain -import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -11,7 +11,7 @@ interface ReferralInteractor { suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData - suspend fun startReferral(portfolioId: PortfolioId): ReferralData + suspend fun startReferral(accountId: AccountId): ReferralData suspend fun getCryptoCurrency( userWalletId: UserWalletId, 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 930418c920..5e67063fe0 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 @@ -1,30 +1,22 @@ package com.tangem.feature.referral.domain -import arrow.core.getOrElse import com.tangem.common.core.TangemSdkError 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.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData import timber.log.Timber -@Suppress("LongParameterList") internal class ReferralInteractorImpl( private val repository: ReferralRepository, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val singleAccountSupplier: SingleAccountSupplier, private val walletManagersFacade: WalletManagersFacade, @@ -40,64 +32,41 @@ internal class ReferralInteractorImpl( return referralData } - override suspend fun startReferral(portfolioId: PortfolioId): ReferralData { + override suspend fun startReferral(accountId: AccountId): ReferralData { if (tokensForReferral.isEmpty()) error("Tokens for ref is empty") val tokenData = tokensForReferral.first() - val userWalletId = portfolioId.userWalletId - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } + val userWalletId = accountId.userWalletId - val accountIndex = when (portfolioId) { - is PortfolioId.Account -> { - val account = singleAccountSupplier.getSyncOrNull( - params = SingleAccountProducer.Params(accountId = portfolioId.accountId), - ) - ?: error("Account not found: ${portfolioId.accountId}") + val account = singleAccountSupplier.getSyncOrNull( + params = SingleAccountProducer.Params(accountId = accountId), + ) + ?: error("Account not found: $accountId") - when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - } - is PortfolioId.Wallet -> null + val accountIndex = when (account) { + is Account.CryptoPortfolio -> account.derivationIndex + is Account.Payment -> TODO("[REDACTED_JIRA]") } val cryptoCurrency = getCryptoCurrency( - userWalletId = portfolioId.userWalletId, + userWalletId = accountId.userWalletId, tokenData = tokenData, accountIndex = accountIndex, ) ?: error("Failed to create crypto currency") - when (portfolioId) { - is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase( - accountId = portfolioId.accountId, - add = cryptoCurrency, - skipDerivationErrors = false, - ).mapLeft { - it.mapToDomainError() - }.onLeft { error -> - if (error is ReferralError.UserCancelledException) { - throw error - } + manageCryptoCurrenciesUseCase( + accountId = accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ) + .mapLeft { it.mapToDomainError() } + .onLeft { error -> + Timber.e(error) + if (error is ReferralError.UserCancelledException) { + throw error } } - is PortfolioId.Wallet -> { - derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> - Timber.e("Failed to derive public keys: $throwable") - throw throwable.mapToDomainError() - } - - addCryptoCurrenciesUseCase( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - ) - } - } - .onLeft(Timber::e) val publicAddress = walletManagersFacade.getDefaultAddress( userWalletId = userWalletId, diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index c82165bda3..f560ae39b1 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -4,10 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.ReferralInteractorImpl import com.tangem.feature.referral.domain.ReferralRepository @@ -23,18 +20,12 @@ class ReferralDomainModule { @ModelScoped fun provideReferralInteractor( referralRepository: ReferralRepository, - derivePublicKeysUseCase: DerivePublicKeysUseCase, - getUserWalletUseCase: GetUserWalletUseCase, - addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, singleAccountSupplier: SingleAccountSupplier, walletManagersFacade: WalletManagersFacade, ): ReferralInteractor { return ReferralInteractorImpl( repository = referralRepository, - derivePublicKeysUseCase = derivePublicKeysUseCase, - getUserWalletUseCase = getUserWalletUseCase, - addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, singleAccountSupplier = singleAccountSupplier, walletManagersFacade = walletManagersFacade, 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 eddb1e949d..f928bf5249 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 @@ -15,7 +15,6 @@ 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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrency import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer @@ -24,8 +23,6 @@ import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCa import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -44,10 +41,12 @@ import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.account.PortfolioSelectorController import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") @Stable @ModelScoped @@ -60,7 +59,6 @@ internal class ReferralModel @Inject constructor( private val urlOpener: UrlOpener, private val getUserWalletUseCase: GetUserWalletUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioFetcherFactory: PortfolioFetcher.Factory, val portfolioSelectorController: PortfolioSelectorController, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, @@ -92,26 +90,24 @@ internal class ReferralModel @Inject constructor( init { analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened()) - if (accountsFeatureToggles.isFeatureEnabled) { - combine( - flow = referralData.filterNotNull().onEach(::selectAccount), - flow2 = portfolioSelectorController.isAccountMode, - transform = { referralData, isAccountMode -> referralData to isAccountMode }, - ).transformLatest { (referralData, isAccountMode) -> - when (isAccountMode) { - false -> showContent(referralData) - true -> combineAccountUI(referralData) + + combine( + flow = referralData.filterNotNull().onEach(::selectAccount), + flow2 = portfolioSelectorController.isAccountMode, + transform = { referralData, isAccountMode -> referralData to isAccountMode }, + ) + .transformLatest { (referralData, isAccountMode) -> + if (!isAccountMode) { + showContent(referralData) + } else { + combineAccountUI(referralData) .map { referralData to it } .collect(::emit) } } - .onEach { (referralData, accountAward) -> showContent(referralData, accountAward) } - .launchIn(modelScope) - } else { - referralData.filterNotNull() - .onEach(::showContent) - .launchIn(modelScope) - } + .onEach { (referralData, accountAward) -> showContent(referralData, accountAward) } + .launchIn(modelScope) + loadReferralData() } @@ -123,12 +119,7 @@ internal class ReferralModel @Inject constructor( flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(), flow4 = portfolioFetcher.data, ) { pair, isBalanceHidden, appCurrency, portfolios -> - val selectedAccount = pair?.second ?: return@combine null - - val cryptoPortfolio = when (selectedAccount) { - is AccountStatus.CryptoPortfolio -> selectedAccount - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } + val cryptoPortfolio = pair?.second ?: return@combine null val awardCryptoCurrency = referralInteractor.getCryptoCurrency( userWalletId = params.userWalletId, @@ -185,11 +176,8 @@ internal class ReferralModel @Inject constructor( val lastInfoState = uiState.referralInfoState uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { - val portfolioId = when (accountsFeatureToggles.isFeatureEnabled) { - true -> PortfolioId(requireNotNull(portfolioSelectorController.selectedAccountSync)) - false -> PortfolioId(params.userWalletId) - } - runCatching { referralInteractor.startReferral(portfolioId) } + val accountId = requireNotNull(portfolioSelectorController.selectedAccountSync) + runCatching { referralInteractor.startReferral(accountId) } .onSuccess { referral -> analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful()) referralData.value = referral diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index c8e3780fe2..4dbf452662 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -1,5 +1,3 @@ package com.tangem.features.send.v2.api -interface SendFeatureToggles { - val isGaslessTransactionsEnabled: Boolean -} \ No newline at end of file +interface SendFeatureToggles \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 597de0c5d3..855c07c8e2 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -211,6 +211,7 @@ sealed class CommonSendAnalyticEvents( const val SEND_CATEGORY = "Token / Send" const val SWAP_CATEGORY = "Swap" const val NFT_SEND_CATEGORY = "NFT" + const val APPROVE_CATEGORY = "Approve" } enum class SendScreenSource { @@ -226,5 +227,6 @@ sealed class CommonSendAnalyticEvents( SendWithSwap("Send&Swap"), WalletConnect("WalletConnect"), NFT("NFT"), + Approve("Approve"), } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index 8b63d8547e..08569966e9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -1,12 +1,6 @@ package com.tangem.features.send.v2 -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.api.SendFeatureToggles import javax.inject.Inject -internal class DefaultSendFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : SendFeatureToggles { - override val isGaslessTransactionsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("GASLESS_TRANSACTIONS_ENABLED") -} \ No newline at end of file +internal class DefaultSendFeatureToggles @Inject constructor() : SendFeatureToggles \ No newline at end of file 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 d066f0da3a..f8ad03193d 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 @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -14,6 +13,7 @@ import javax.inject.Inject @ModelScoped internal class SendConfirmAlertFactory @Inject constructor( private val messageSender: UiMessageSender, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -34,34 +34,16 @@ internal class SendConfirmAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - messageSender.send( - DialogMessage( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + messageSender.send(errorDialog) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index ca35f59a98..cd1e7a333d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -14,7 +14,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.conditional import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.FeeSelectorComponent -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel @@ -32,7 +31,6 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams, @Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit, private val feeSelectorComponentFactory: FeeSelectorComponent.Factory, - private val sendFeatureToggles: SendFeatureToggles, ) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { private val model: FeeSelectorBlockModel = getOrCreateModel(params = params) @@ -91,7 +89,6 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, - isGaslessFeatureEnabled = sendFeatureToggles.isGaslessTransactionsEnabled, modifier = modifier .conditional(isScreenSource && (isNotSingleFee || isGaslessAvailable)) { Modifier.clickable { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index a3cbaac500..ac73f11c28 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -19,7 +19,6 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeNonce @@ -59,7 +58,6 @@ internal class FeeSelectorLogic @AssistedInject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase, - sendFeatureToggles: SendFeatureToggles, isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, ) : FeeSelectorIntents { @@ -67,8 +65,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( private val loadFeeJobHolder = JobHolder() val uiState = MutableStateFlow(params.state) - val isGaslessEnabled = sendFeatureToggles.isGaslessTransactionsEnabled && - params.onLoadFeeExtended != null && + val isGaslessEnabled = params.onLoadFeeExtended != null && isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network) && params.cryptoCurrencyStatus.currency is CryptoCurrency.Token @@ -227,7 +224,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( }, ) }, - ifLeft = { feeError -> + ifLeft = { _ -> feeSelectorCheckReloadTrigger.callbackCheckResult(false) feeSelectorAlertFactory.getFeeUnreachableErrorState { loadFee(isReload = true) } }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index c635854c69..a74fac4f79 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -57,7 +57,6 @@ private const val READ_MORE_TAG = "READ_MORE" @Composable internal fun FeeSelectorBlockContent( state: FeeSelectorUM, - isGaslessFeatureEnabled: Boolean, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -80,25 +79,16 @@ internal fun FeeSelectorBlockContent( contentDescription = null, tint = TangemTheme.colors.icon.accent, ) - FeeSelectorDescription( - state = state, - isGaslessFeatureEnabled = isGaslessFeatureEnabled, - onReadMoreClick = onReadMoreClick, - ) + FeeSelectorDescription(state = state, onReadMoreClick = onReadMoreClick) } } @Composable -private fun FeeSelectorDescription( - state: FeeSelectorUM, - isGaslessFeatureEnabled: Boolean, - onReadMoreClick: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun FeeSelectorDescription(state: FeeSelectorUM, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) { Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) { FeeSelectorStaticPart(modifier = Modifier.weight(1f), onReadMoreClick = onReadMoreClick) when (state) { - is FeeSelectorUM.Content -> FeeContent(state, isGaslessFeatureEnabled) + is FeeSelectorUM.Content -> FeeContent(state) is FeeSelectorUM.Loading -> FeeLoading() is FeeSelectorUM.Error -> FeeError() } @@ -181,17 +171,15 @@ private fun FeeLoading() { } @Composable -private fun FeeContent(state: FeeSelectorUM.Content, isGaslessFeatureEnabled: Boolean, modifier: Modifier = Modifier) { +private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) { val fiatRate = state.feeFiatRateUM Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - if (isGaslessFeatureEnabled) { - AuditLabel( - state = AuditLabelUM( - text = stringReference(state.selectedFeeItem.fee.amount.currencySymbol), - type = AuditLabelUM.Type.General, - ), - ) - } + AuditLabel( + state = AuditLabelUM( + text = stringReference(state.selectedFeeItem.fee.amount.currencySymbol), + type = AuditLabelUM.Type.General, + ), + ) EllipsisText( text = if (state.feeExtraInfo.isFeeConvertibleToFiat && fiatRate != null) { @@ -218,7 +206,7 @@ private fun FeeContent(state: FeeSelectorUM.Content, isGaslessFeatureEnabled: Bo .testTag(FeeSelectorBlockTestTags.FEE_AMOUNT), ) - val isGaslessAvailable = isGaslessFeatureEnabled && state.feeExtraInfo.transactionFeeExtended != null + val isGaslessAvailable = state.feeExtraInfo.transactionFeeExtended != null if (!state.feeItems.isSingleItem() || isGaslessAvailable) { Icon( @@ -240,7 +228,6 @@ private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvi TangemThemePreview { FeeSelectorBlockContent( modifier = Modifier.fillMaxWidth(), - isGaslessFeatureEnabled = true, state = state, onReadMoreClick = {}, ) 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 3411103f5c..710868cdef 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 @@ -13,6 +13,8 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -23,7 +25,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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -36,7 +37,6 @@ 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 import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase @@ -75,10 +75,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -100,7 +98,6 @@ internal class SendConfirmModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener, @@ -114,7 +111,6 @@ internal class SendConfirmModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendAmountReduceTrigger: SendAmountReduceTrigger, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val currenciesRepository: CurrenciesRepository, private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, @@ -301,6 +297,7 @@ internal class SendConfirmModel @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase.invoke(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Send)) sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } @@ -466,24 +463,14 @@ internal class SendConfirmModel @Inject constructor( .firstOrNull { it.address == confirmData.enteredDestination } ?: return - val userWalletId = receivingUserWallet.userWalletId ?: return val network = receivingUserWallet.network ?: return modelScope.launch(dispatchers.default) { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = receivingUserWallet.accountId ?: return@launch + val accountId = receivingUserWallet.accountId ?: return@launch - val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) - manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) - } else { - withContext(NonCancellable) { - addCryptoCurrenciesUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - network = network, - ) - } - }.onLeft(Timber::e) + val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) + manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) + .onLeft(Timber::e) } } 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 c8eeee19df..180c7115a1 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 @@ -5,40 +5,35 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles 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.util.cardTypesResolver import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.account.Account -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase @@ -55,7 +50,6 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -74,7 +68,6 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates -import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned internal interface SendComponentCallback : SendAmountComponent.ModelCallback, @@ -90,7 +83,6 @@ internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, @@ -108,7 +100,6 @@ internal class SendModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model(), SendComponentCallback { private val params: SendComponent.Params = paramsContainer.require() @@ -254,7 +245,7 @@ internal class SendModel @Inject constructor( showAlertError() } - suspend fun prepareTransferTransaction(): Either { + private suspend fun prepareTransferTransaction(): Either { val predefinedValues = predefinedValues val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value return if (predefinedValues is PredefinedValues.Content.Deeplink) { @@ -360,34 +351,24 @@ internal class SendModel @Inject constructor( ifRight = { wallet -> userWallet = wallet - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = cryptoCurrency, + ).onEach { (account, cryptoCurrencyStatus) -> + isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() + accountFlow.value = account + + cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus + feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = params.userWalletId, - currency = cryptoCurrency, - ).onEach { (account, cryptoCurrencyStatus) -> - isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - accountFlow.value = account + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus - cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - - if (params.amount != null) { - router.replaceAll(Confirm) - } - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - val isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - val isMultiCurrency = wallet.isMultiCurrency - getCurrenciesStatusUpdates( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ) - } + if (params.amount != null) { + router.replaceAll(Confirm) + } + }.flowOn(dispatchers.default) + .launchIn(modelScope) }, ifLeft = { error -> Timber.w(error.toString()) @@ -409,77 +390,6 @@ internal class SendModel @Inject constructor( .saveIn(balanceHidingJobHolder) } - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - getCurrencyStatus( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoCurrencyStatus -> - cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - feeCryptoCurrencyStatusFlow.value = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency) - - if (params.amount != null) { - router.replaceAll(CommonSendRoute.Confirm) - } - }, - ifLeft = { - sendConfirmAlertFactory.getGenericErrorState( - onFailedTxEmailClick = { - onFailedTxEmailClick(it.toString()) - }, - popBack = router::pop, - ) - }, - ) - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } - - private fun getCurrencyStatus( - isSingleWalletWithToken: Boolean, - isMultiCurrency: Boolean, - ): Flow> { - return when { - isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = true, - ) - isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ) - else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId) - } - } - - fun getSelectedFeeToken(): CryptoCurrency { - val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content - val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended - val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency - return if (isFeeInTokenCurrency) { - feeUMV2.feeExtraInfo.feeCryptoCurrencyStatus.currency - } else { - feeCryptoCurrencyStatusFlow.value.currency - } - } - - private suspend fun getFeeCurrencyStatus( - cryptoCurrencyStatus: CryptoCurrencyStatus, - isMultiCurrency: Boolean, - ): CryptoCurrencyStatus { - return if (isMultiCurrency) { - getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - } else { - cryptoCurrencyStatus - } - } - private fun subscribeOnQRScannerResult() { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } @@ -518,6 +428,7 @@ internal class SendModel @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Send)) sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 62ee3117f2..084f793b58 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -8,6 +8,8 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -202,6 +204,7 @@ internal class NFTSendConfirmModel @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Send)) sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 5128c82ff5..4d84c0b4a2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -6,17 +6,18 @@ import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles 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.card.common.util.cardTypesResolver import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -27,7 +28,6 @@ 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.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.transaction.error.GetFeeError @@ -66,7 +66,6 @@ internal class NFTSendModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, @@ -77,7 +76,7 @@ internal class NFTSendModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -186,31 +185,24 @@ internal class NFTSendModel @Inject constructor( ?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network } ?: return@launch - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId, - cryptoCurrency, - ).onEach { (maybeAccount, cryptoStatus) -> - account = maybeAccount - isAccountsMode = isAccountsModeEnabledUseCase.invokeSync() + getAccountCurrencyStatusUseCase( + userWalletId, + cryptoCurrency, + ).onEach { (maybeAccount, cryptoStatus) -> + account = maybeAccount + isAccountsMode = isAccountsModeEnabledUseCase.invokeSync() - cryptoCurrencyStatus = cryptoStatus - feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoStatus, - ).getOrNull() ?: cryptoStatus + cryptoCurrencyStatus = cryptoStatus + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() ?: cryptoStatus - if (uiState.value.destinationUM is DestinationUM.Empty) { - router.replaceAll(Destination(isEditMode = false)) - } - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - getCurrenciesStatusUpdates( - isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + if (uiState.value.destinationUM is DestinationUM.Empty) { + router.replaceAll(Destination(isEditMode = false)) + } + }.flowOn(dispatchers.default) + .launchIn(modelScope) }, ifLeft = { alertFactory.getGenericErrorState(::onFailedTxEmailClick) @@ -242,38 +234,11 @@ internal class NFTSendModel @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Send)) sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = isSingleWalletWithToken, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoStatus -> - cryptoCurrencyStatus = cryptoStatus - feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoStatus, - ).getOrNull() ?: cryptoStatus - - if (uiState.value.destinationUM is DestinationUM.Empty) { - router.replaceAll(Destination(isEditMode = false)) - } - }, - ifLeft = { - alertFactory.getGenericErrorState( - onFailedTxEmailClick = { onFailedTxEmailClick(it.toString()) }, - popBack = { router.pop() }, - ) - }, - ) - }.launchIn(modelScope) - } - private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, 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 96690cb647..e2c9a282c5 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 @@ -11,18 +11,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router 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.filterCryptoPortfolio import com.tangem.domain.models.network.CryptoCurrencyAddress -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -63,7 +59,6 @@ internal class SendDestinationModel @Inject constructor( private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase, @@ -71,7 +66,6 @@ internal class SendDestinationModel @Inject constructor( private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -191,15 +185,7 @@ internal class SendDestinationModel @Inject constructor( private fun getWalletsAndRecent() { combine( - flow = if (accountsFeatureToggles.isFeatureEnabled) { - getAddedAddresses() - } else { - getWalletsUseCase().conflate().map { - waitForDelay(RECENT_LOAD_DELAY) { - it.toAvailableWallets() - } - } - }, + flow = getAddedAddresses(), flow2 = getFixedTxHistoryItemsUseCase( userWalletId = userWalletId, currency = cryptoCurrency, @@ -226,48 +212,6 @@ internal class SendDestinationModel @Inject constructor( }.flowOn(dispatchers.default).launchIn(modelScope) } - private suspend fun List.toAvailableWallets(): List { - return coroutineScope { - val cryptoCurrencyNetwork = cryptoCurrency.network - - return@coroutineScope filterNot { it.isLocked } - .map { wallet -> - async { - val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { cryptoCurrency -> - if (cryptoCurrency.network.rawId == cryptoCurrencyNetwork.rawId) { - getNetworkAddressesUseCase.invokeSync( - userWalletId = wallet.walletId, - networkRawId = cryptoCurrency.network.id.rawId, - ) - } else { - null - } - } - } else { - getNetworkAddressesUseCase.invokeSync( - userWalletId = wallet.walletId, - networkRawId = cryptoCurrencyNetwork.id.rawId, - ) - } - wallet to addresses - } - }.awaitAll() - .asSequence() - .mapNotNull { (wallet, addresses) -> - addresses?.map { (cryptoCurrency, address) -> - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrency, - userWalletId = wallet.walletId, - ) - } - }.flatten() - .toList() - } - } - private fun getAddedAddresses(): Flow> { return combine( flow = getWalletsUseCase().conflate(), diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 92180a4384..72ddfd69ee 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -81,6 +81,10 @@ dependencies { /** Feature modules */ implementation(projects.features.staking.api) implementation(projects.features.txhistory.api) + implementation(projects.features.approval.api) + + /** Decompose */ + implementation(deps.decompose.ext.compose) /** DI */ implementation(deps.hilt.android) diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml index 113f53de5d..8cd2dca55b 100644 --- a/features/staking/impl/detekt-baseline-debug.xml +++ b/features/staking/impl/detekt-baseline-debug.xml @@ -4,7 +4,6 @@ BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean - BooleanPropertyNaming:StakingModel.kt$StakingModel$val noBalanceState = balanceState == null BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as @@ -14,28 +13,13 @@ MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } } MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } } MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ EnterAmountBoundary( amount = it, fiatRate = status.value.fiatRate.orZero(), ) } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ Timber.e(it) false } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ isBalanceHiddenFlow.value = it.isBalanceHidden stateController.update( transformer = HideBalanceStateTransformer( isBalanceHidden = it.isBalanceHidden, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, ), ) } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ stateController.update( SetFeeToTonInitializeBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = it.normal, isFeeApproximate = false, ), ) } MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) } - NullCheckOnMutableProperty:StakingModel.kt$StakingModel$if (feeCryptoCurrencyStatus != null && fee != null) { getBalanceNotEnoughForFeeWarningUseCase( fee = fee, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, ).getOrNull() } else { null } NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState - PropertyUsedBeforeDeclaration:StakingModel.kt$StakingModel$isAmountSubtractAvailable PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState - SuspendFunSwallowedCancellation:StakingModel.kt$StakingModel$runCatching UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -> Unit - UnnecessaryLet:StakingModel.kt$StakingModel$let(::add) UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) } UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) } - UnsafeCallOnNullableType:StakingModel.kt$StakingModel$tonAccountInitializeTransaction!! - VarCouldBeVal:StakingModel.kt$StakingModel$private var actionsJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var approvalJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var feeJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var sendTransactionJobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var stakingStateRouter: StakingStateRouter = StakingStateRouter( appRouter = appRouter, stateController = stateController, analyticsEventsHandler = analyticsEventHandler, ) - VarCouldBeVal:StakingModel.kt$StakingModel$private var stepChangesJobHolder = JobHolder() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt index 3f40fea39d..f4f55c926f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt @@ -4,8 +4,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.features.approval.api.GiveApprovalComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.impl.presentation.model.StakingModel import com.tangem.features.staking.impl.presentation.ui.StakingScreen @@ -16,14 +20,34 @@ import dagger.assisted.AssistedInject internal class DefaultStakingComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: StakingComponent.Params, + private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, ) : StakingComponent, AppComponentContext by appComponentContext { private val model: StakingModel = getOrCreateModel(params) + private val approvalSlot = childSlot( + key = "stakingApprovalSlot", + source = model.approvalSlotNavigation, + serializer = null, + handleBackButton = true, + childFactory = { _, factoryContext -> + val approvalParams = model.getApprovalParams() + ?: error("Approval params are not available") + giveApprovalComponentFactory.create( + context = childByContext(factoryContext), + params = approvalParams, + ) + }, + ) + @Composable override fun Content(modifier: Modifier) { val currentState by model.uiState.collectAsStateWithLifecycle() + val approvalSlotState by approvalSlot.subscribeAsState() + StakingScreen(currentState) + + approvalSlotState.child?.instance?.BottomSheet() } @AssistedFactory 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 3ace74e0f1..805dafd774 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 @@ -7,15 +7,21 @@ 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 import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +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.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -23,12 +29,12 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format 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.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -67,7 +73,6 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader @@ -114,7 +119,6 @@ internal class StakingModel @Inject constructor( private val stateController: StakingStateController, override val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -150,16 +154,18 @@ internal class StakingModel @Inject constructor( @DelayedWork private val coroutineScope: CoroutineScope, private val innerRouter: InnerStakingRouter, private val messageSender: UiMessageSender, - private val accountsFeatureToggles: AccountsFeatureToggles, + private val giveApprovalFeatureToggles: GiveApprovalFeatureToggles, appRouter: AppRouter, ) : Model(), StakingClickIntents { val uiState: StateFlow = stateController.uiState val value: StakingUiState get() = uiState.value + val approvalSlotNavigation = SlotNavigation() + private val params = paramsContainer.require() - private var stakingStateRouter: StakingStateRouter = StakingStateRouter( + private val stakingStateRouter: StakingStateRouter = StakingStateRouter( appRouter = appRouter, stateController = stateController, analyticsEventsHandler = analyticsEventHandler, @@ -238,6 +244,7 @@ internal class StakingModel @Inject constructor( ) } + @Suppress("PropertyUsedBeforeDeclaration") private val transactionSender: StakingTransactionSender by lazy(LazyThreadSafetyMode.NONE) { stakingOperationsFactory.createTransactionSender( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -249,7 +256,7 @@ internal class StakingModel @Inject constructor( private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( - stateController = stateController, + messageSender = messageSender, popBackStack = ::onBackClick, onFailedTxEmailClick = ::onFailedTxEmailClick, ) @@ -258,6 +265,31 @@ internal class StakingModel @Inject constructor( analyticsEventHandler = analyticsEventHandler, ) + private val approvalCallback = object : GiveApprovalComponent.Callback { + override fun onApproveClick() { + } + + override fun onApproveDone() { + approvalSlotNavigation.dismiss() + stakingAnalyticSender.sendTransactionApprovalAnalytics( + cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return, + ) + stateController.update(SetApprovalInProgressTransformer) + awaitForAllowance() + } + + override fun onApproveFailed() { + approvalSlotNavigation.dismiss() + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) + } + + override fun onCancelClick() { + approvalSlotNavigation.dismiss() + } + } + private var stakingApproval: StakingApproval = StakingApproval.Empty private var stakingAllowance: BigDecimal = BigDecimal.ZERO private var isAmountSubtractAvailable: Boolean = false @@ -299,17 +331,17 @@ internal class StakingModel @Inject constructor( override fun onNextClick(balanceState: BalanceState?) { modelScope.launch { val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo - val noBalanceState = balanceState == null + val isBalanceAbsent = balanceState == null val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit when { - isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> { + isInitialInfoStep && isBalanceAbsent && integration.areAllTargetsFull && hasNoYieldBalanceData -> { stakingEventFactory.createStakingValidatorsUnavailableAlert() return@launch } - isInitialInfoStep && noBalanceState -> { + isInitialInfoStep && isBalanceAbsent -> { val list = buildList { - SetConfirmationStateInitTransformer( + val setConfirmationStateInitTransformer = SetConfirmationStateInitTransformer( isEnter = true, isExplicitExit = false, balanceState = null, @@ -317,13 +349,17 @@ internal class StakingModel @Inject constructor( stakingApproval = stakingApproval, stakingAllowance = stakingAllowance, integration = integration, - ).let(::add) + ) + + add(setConfirmationStateInitTransformer) + if (integration.isPartialAmountDisabled) { - ValidatorSelectChangeTransformer( + val validatorSelectChangeTransformer = ValidatorSelectChangeTransformer( selectedTarget = integration.preferredTargets.firstOrNull(), integration = integration, - ).let(::add) - SetAmountDataTransformer( + ) + + val setAmountDataTransformer = SetAmountDataTransformer( clickIntents = this@StakingModel, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, @@ -331,15 +367,25 @@ internal class StakingModel @Inject constructor( isBalanceHidden = isBalanceHiddenFlow.value, isAccountsModeEnabled = isAccountsModeEnabled, account = account, - ).let(::add) - AmountMaxValueStateTransformer( + ) + + val amountMaxValueStateTransformer = AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, minimumTransactionAmount = minimumTransactionAmount, actionType = uiState.value.actionType, integration = integration, - ).let(::add) + ) + + addAll( + listOf( + validatorSelectChangeTransformer, + setAmountDataTransformer, + amountMaxValueStateTransformer, + ), + ) } } + stateController.updateAll(*list.toTypedArray()) } } @@ -503,11 +549,7 @@ internal class StakingModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.FeeIncreased(stateController::dismissAlert), - ), - ) + messageSender.send(StakingAlertUM.feeIncreased {}) updateNotifications() }, onTransactionExpired = { @@ -571,9 +613,7 @@ internal class StakingModel @Inject constructor( override fun onAmountEnterClick() { if (integration.preferredTargets.isEmpty()) { - stateController.updateEvent( - StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), - ) + messageSender.send(StakingAlertUM.noAvailableValidators()) } else { if (uiState.value.actionType is StakingActionCommonType.Enter) { stateController.updateAll( @@ -744,16 +784,20 @@ internal class StakingModel @Inject constructor( } override fun showApprovalBottomSheet() { - stateController.update( - ShowApprovalBottomSheetTransformer( - userWallet = userWallet, - appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - ) { - stateController.update(DismissBottomSheetStateTransformer) - }, - ) + if (giveApprovalFeatureToggles.isGaslessApprovalEnabled) { + approvalSlotNavigation.activate(Unit) + } else { + stateController.update( + ShowApprovalBottomSheetTransformer( + userWallet = userWallet, + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) { + stateController.update(DismissBottomSheetStateTransformer) + }, + ) + } } override fun onApproveTypeChange(approveType: ApproveType) { @@ -907,7 +951,7 @@ internal class StakingModel @Inject constructor( AmountReduceByStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, minimumTransactionAmount = minimumTransactionAmount, - value = AmountReduceByTransformer.ReduceByData( + value = ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, ), @@ -960,7 +1004,7 @@ internal class StakingModel @Inject constructor( task = PeriodicTask( delay = ALLOWANCE_UPDATE_DELAY, task = { - runCatching { + runSuspendCatching { getAllowanceUseCase( userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyStatus.currency, @@ -1038,6 +1082,7 @@ internal class StakingModel @Inject constructor( unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction }, ) + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Staking)) sendFeedbackEmailUseCase(email) } } @@ -1047,11 +1092,7 @@ internal class StakingModel @Inject constructor( } override fun showPrimaryClickAlert() { - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.StakeMoreClickUnavailable(cryptoCurrencyStatus.currency), - ), - ) + messageSender.send(StakingAlertUM.stakeMoreClickUnavailable(cryptoCurrencyStatus.currency)) } override fun onOpenLearnMoreAboutApproveClick() { @@ -1084,17 +1125,20 @@ internal class StakingModel @Inject constructor( network = cryptoCurrencyStatus.currency.network, memo = null, ) - tonAccountInitializeTransaction = transaction.getOrElse { + + val initialTransaction = transaction.getOrElse { stateController.update( SetFeeErrorToTonInitializeBottomSheetTransformer(), ) return@launch } + tonAccountInitializeTransaction = initialTransaction + val transactionFee = getFeeUseCase( userWallet = userWallet, network = cryptoCurrencyStatus.currency.network, - transactionData = tonAccountInitializeTransaction!!, + transactionData = initialTransaction, ) transactionFee.fold( @@ -1103,12 +1147,12 @@ internal class StakingModel @Inject constructor( SetFeeErrorToTonInitializeBottomSheetTransformer(), ) }, - ifRight = { + ifRight = { fee -> stateController.update( SetFeeToTonInitializeBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = it.normal, + fee = fee.normal, isFeeApproximate = false, ), ) @@ -1201,41 +1245,20 @@ internal class StakingModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId = params.userWalletId, - currency = params.cryptoCurrency, - ).conflate().distinctUntilChanged() - .filter { - value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() - }.onEach { (maybeAccount, maybeStatus) -> - isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() - account = maybeAccount - onDataLoaded(maybeStatus) - }.flowOn(dispatchers.main) - .launchIn(modelScope) - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrencyId, - isSingleWalletWithTokens = false, - ).conflate().distinctUntilChanged() - .filter { - value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() - } - .onEach { maybeStatus -> - maybeStatus.fold( - ifRight = { onDataLoaded(it) }, - ifLeft = { error -> - stakingEventFactory.createGenericErrorAlert(error.toString()) - stateController.update( - SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), - ) - }, - ) - }.flowOn(dispatchers.main) - .launchIn(modelScope) - } + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = params.cryptoCurrency, + ) + .conflate() + .distinctUntilChanged() + .filter { value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() } + .onEach { (maybeAccount, maybeStatus) -> + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + account = maybeAccount + onDataLoaded(maybeStatus) + } + .flowOn(dispatchers.main) + .launchIn(modelScope) } private suspend fun onDataLoaded(status: CryptoCurrencyStatus) { @@ -1252,12 +1275,11 @@ internal class StakingModel @Inject constructor( ) } - feeCryptoCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() - minimumTransactionAmount = - getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()?.let { + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + minimumTransactionAmount = getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull() + ?.let { amount -> EnterAmountBoundary( - amount = it, + amount = amount, fiatRate = status.value.fiatRate.orZero(), ) } @@ -1276,11 +1298,11 @@ internal class StakingModel @Inject constructor( getBalanceHidingSettingsUseCase() .conflate() .distinctUntilChanged() - .onEach { - isBalanceHiddenFlow.value = it.isBalanceHidden + .onEach { settings -> + isBalanceHiddenFlow.value = settings.isBalanceHidden stateController.update( transformer = HideBalanceStateTransformer( - isBalanceHidden = it.isBalanceHidden, + isBalanceHidden = settings.isBalanceHidden, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, ), @@ -1414,8 +1436,8 @@ internal class StakingModel @Inject constructor( val isAccountInitializedNewValue = checkAccountInitializedUseCase.invoke( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e(it) + ).getOrElse { throwable -> + Timber.e(throwable) false } @@ -1458,6 +1480,26 @@ internal class StakingModel @Inject constructor( isTonHeatupCase } + fun getApprovalParams(): GiveApprovalComponent.Params? { + val amountState = value.amountState as? AmountState.Data ?: return null + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data ?: return null + val targetAddress = validatorState.chosenTarget.address + val feeCurrencyStatus = feeCryptoCurrencyStatus ?: return null + + return GiveApprovalComponent.Params( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCryptoCurrencyStatus = feeCurrencyStatus, + amount = amountState.amountTextField.value, + spenderAddress = targetAddress, + subtitle = resourceReference( + id = R.string.give_permission_staking_subtitle, + formatArgs = wrappedList(cryptoCurrencyStatus.currency.symbol), + ), + callback = approvalCallback, + ) + } + private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L 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 00409eed5c..7a02bd36ef 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 @@ -3,12 +3,9 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -72,16 +69,6 @@ internal class StakingStateController @Inject constructor( mutableUiState.update(function = titleTransformer::transform) } - fun updateEvent(event: StakingEvent?) { - mutableUiState.update { - it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) - } - } - - fun dismissAlert() { - mutableUiState.update { it.copy(event = consumedEvent()) } - } - private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, @@ -98,7 +85,6 @@ internal class StakingStateController @Inject constructor( rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, - event = consumedEvent(), bottomSheetConfig = null, actionType = StakingActionCommonType.Enter(skipEnterAmount = false), buttonsState = NavigationButtonsState.Empty, 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 699729948f..8624ce9a72 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 @@ -6,14 +6,12 @@ import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData -import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.StakingTarget import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -40,7 +38,6 @@ internal data class StakingUiState( val bottomSheetConfig: TangemBottomSheetConfig?, val actionType: StakingActionCommonType, val buttonsState: NavigationButtonsState, - val event: StateEvent, val balanceState: BalanceState?, val showColdWalletInteractionIcon: Boolean, val shouldShowHoldToConfirmButton: Boolean, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 3e757a6adc..acba4a78d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -1,85 +1,74 @@ package com.tangem.features.staking.impl.presentation.state.events -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.staking.impl.R -@Immutable -internal sealed class StakingAlertUM : AlertUM { +internal object StakingAlertUM { - data class GenericError( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun genericError(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.common_unknown_error), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class StakingError( - val code: String, - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun stakingError(code: String, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.generic_error_code, wrappedList(code)), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data object NoAvailableValidators : StakingAlertUM() { - override val title = resourceReference(R.string.common_error) - override val message = resourceReference(R.string.staking_no_validators_error_message) - override val confirmButtonText = resourceReference(R.string.common_ok) - override val onConfirmClick = null - } + fun noAvailableValidators(): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.staking_no_validators_error_message), + ) - data class FeeIncreased( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference? = null - override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun feeIncreased(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = null, + message = resourceReference(id = R.string.send_notification_high_fee_title), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) - data object ValidatorsUnavailable : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) - override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun validatorsUnavailable(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.staking_error_no_validators_title), + message = resourceReference(id = R.string.staking_error_no_validators_message), + ) - data class StakeMoreClickUnavailable( - val cryptoCurrency: CryptoCurrency, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( + fun stakeMoreClickUnavailable(cryptoCurrency: CryptoCurrency): DialogMessage = DialogMessage( + title = null, + message = resourceReference( id = R.string.staking_stake_more_button_unavailability_reason, wrappedList(cryptoCurrency.name, cryptoCurrency.symbol), - ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + ), + ) - data class RewardsMinimumRequirementsError( - val cryptoCurrencyName: String, - val cryptoAmountValue: String, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( - id = R.string.staking_details_min_rewards_notification, - formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + fun rewardsMinimumRequirementsError(cryptoCurrencyName: String, cryptoAmountValue: String): DialogMessage = + DialogMessage( + title = null, + message = resourceReference( + id = R.string.staking_details_min_rewards_notification, + formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + ), ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } - data class NetworkFeeUpdated( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_title) - override val message: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun networkFeeUpdated(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.staking_alert_network_fee_updated_title), + message = resourceReference(R.string.staking_alert_network_fee_updated_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt deleted file mode 100644 index 7e7700595e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed class StakingEvent { - - data class ShowSnackBar(val text: TextReference) : StakingEvent() - - data class ShowAlert(val alert: AlertUM) : StakingEvent() -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 4ba7fb1dd9..fd94adadd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -1,69 +1,63 @@ package com.tangem.features.staking.impl.presentation.state.events -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.features.staking.impl.presentation.state.StakingStateController internal class StakingEventFactory( - private val stateController: StakingStateController, + private val messageSender: UiMessageSender, private val popBackStack: () -> Unit, private val onFailedTxEmailClick: (String) -> Unit, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), ) { fun createGenericErrorAlert(error: String) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.GenericError( + messageSender.send( + StakingAlertUM.genericError( onConfirmClick = { onFailedTxEmailClick(error) }, ), ) - stateController.updateEvent(alert) } fun createSendTransactionErrorAlert(error: SendTransactionError?) { val alert = error?.let { - TransactionErrorAlertConverter( + transactionErrorDialogFactory.create( + error = error, popBackStack = popBackStack, onFailedTxEmailClick = onFailedTxEmailClick, - ).convert(error) - }?.let { - StakingEvent.ShowAlert(it) + ) } - stateController.updateEvent(alert) + alert?.let { messageSender.send(it) } } fun createStakingErrorAlert(error: StakingError) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.StakingError( + messageSender.send( + StakingAlertUM.stakingError( code = error.toString(), onConfirmClick = { onFailedTxEmailClick(error.toString()) }, ), ) - stateController.updateEvent(alert) } fun createStakingValidatorsUnavailableAlert() { - val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) - stateController.updateEvent(alert) + messageSender.send(StakingAlertUM.validatorsUnavailable()) } fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) { - stateController.updateEvent( - StakingEvent.ShowAlert( - alert = StakingAlertUM.RewardsMinimumRequirementsError( - cryptoCurrencyName = cryptoCurrencyName, - cryptoAmountValue = cryptoAmountValue, - ), + messageSender.send( + StakingAlertUM.rewardsMinimumRequirementsError( + cryptoCurrencyName = cryptoCurrencyName, + cryptoAmountValue = cryptoAmountValue, ), ) } fun createNetworkFeeUpdatedAlert(onConfirm: () -> Unit) { - val alert = StakingEvent.ShowAlert( - alert = StakingAlertUM.NetworkFeeUpdated( + messageSender.send( + StakingAlertUM.networkFeeUpdated( onConfirmClick = onConfirm, ), ) - stateController.updateEvent(alert) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt deleted file mode 100644 index 96234a843e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.features.staking.impl.presentation.ui - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent - -@Composable -internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { - val resources = LocalContext.current.resources - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - StakingAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is StakingEvent.ShowSnackBar -> { - snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) - } - is StakingEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButtonUM - val dismissButton: DialogButtonUM? - - val onActionClick = state.onConfirmClick - if (onActionClick != null) { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - onActionClick() - onDismiss() - }, - ) - - dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - } else { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ) - dismissButton = null - } - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 0196216ea0..836d37dec7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { - val snackbarHostState = remember { SnackbarHostState() } val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data BackHandler(onBack = uiState.clickIntents::onPrevClick) @@ -71,11 +69,6 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } - - StakingEventEffect( - event = uiState.event, - snackbarHostState = snackbarHostState, - ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 0994844d9b..0fb397effb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -1,7 +1,9 @@ package com.tangem.features.swap.v2.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -25,6 +27,8 @@ internal class SwapAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, + private val analyticsEventHandler: AnalyticsEventHandler, ) { fun getGenericErrorState(expressError: ExpressError, onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { uiMessageSender.send( @@ -44,36 +48,21 @@ internal class SwapAlertFactory @Inject constructor( ) } + @Suppress("CanBeNonNullable") fun getSendTransactionErrorState( error: SendTransactionError?, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + if (error == null) return + + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick( @@ -98,6 +87,7 @@ internal class SwapAlertFactory @Inject constructor( val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.Swap)) sendFeedbackEmailUseCase( type = FeedbackEmailType.SwapProblem( walletMetaInfo = metaInfo, 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 64e5650b5b..24a68d2945 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 @@ -40,6 +40,27 @@ internal sealed class SendWithSwapAnalyticEvents( }, ), AppsFlyerIncludedEvent + data class OnSendClick( + val providerName: String, + val feeType: AnalyticsParam.FeeType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, + ) : SendWithSwapAnalyticEvents( + event = "Button - Send with Swap", + params = buildMap { + put(PROVIDER, providerName) + put(FEE_TYPE, if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast") + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + 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 + data class NoticeCanNotSwapToken( val fromToken: CryptoCurrency, val toTokenSymbol: String, 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 271decfaa7..99fcf5c1e4 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 @@ -305,6 +305,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(true)) val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended + modelScope.launch(dispatchers.default) { sendClickAnalytics() } swapTransactionSender.sendTransaction( feeExtended = feeExtended, confirmData = confirmData, @@ -494,6 +495,30 @@ internal class SendWithSwapConfirmModel @Inject constructor( ) } + private suspend fun sendClickAnalytics() { + 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.OnSendClick( + providerName = selectedProvider.name, + feeType = feeType, + fromToken = fromCurrency, + toToken = toCurrency, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, + ), + ) + } + private fun getSelectedFeeToken(): CryptoCurrency { val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended 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 31b4ba8651..dc868296e8 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 @@ -1,29 +1,23 @@ package com.tangem.features.swap.v2.impl.sendviaswap.model -import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles 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.util.cardTypesResolver import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -49,7 +43,6 @@ import kotlin.properties.Delegates internal class SendWithSwapModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -57,7 +50,6 @@ internal class SendWithSwapModel @Inject constructor( private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val swapAlertFactory: SwapAlertFactory, - private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model(), SwapAmountComponent.ModelCallback, @@ -202,79 +194,20 @@ internal class SendWithSwapModel @Inject constructor( } private fun getPrimaryCurrencyStatusUpdates(cryptoCurrency: CryptoCurrency) { - val wallet = userWallet - val isMultiCurrency = wallet.isMultiCurrency - val isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = cryptoCurrency, + ).onEach { (account, cryptoCurrencyStatus) -> + accountFlow.value = account + isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( + primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus + primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = params.userWalletId, - currency = cryptoCurrency, - ).onEach { (account, cryptoCurrencyStatus) -> - accountFlow.value = account - isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - - primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - getCurrencyStatus( - cryptoCurrency = cryptoCurrency, - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoCurrencyStatus -> - primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - }, - ifLeft = { error -> - swapAlertFactory.getGenericErrorState( - expressError = ExpressError.UnknownError, - onFailedTxEmailClick = { - modelScope.launch { - swapAlertFactory.onFailedTxEmailClick( - userWallet = userWallet, - cryptoCurrency = params.currency, - errorMessage = error.toString(), - ) - } - }, - popBack = ::onBackClick, - ) - }, - ) - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } - } - - private fun getCurrencyStatus( - cryptoCurrency: CryptoCurrency, - isSingleWalletWithToken: Boolean, - isMultiCurrency: Boolean, - ): Flow> { - return when { - isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = true, - ) - isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ) - else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId) - } + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus + }.flowOn(dispatchers.default) + .launchIn(modelScope) } private fun subscribeOnBalanceHidden() { 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 a04749322a..1d102e458e 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 @@ -8,7 +8,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.account.Account @@ -23,14 +22,12 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, ) : SwapTransactionRepository { @@ -110,11 +107,7 @@ internal class DefaultSwapTransactionRepository( flow2 = appPreferencesStore.getObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), - flow3 = if (accountsFeatureToggles.isFeatureEnabled) { - singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId)) - } else { - flowOf(null) - }, + flow3 = singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId)), ) { savedTransactions, txStatuses, accountList -> val currencyToTxs = savedTransactions?.filter { savedTx -> diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 669ba6fcd4..6057e6344e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade @@ -59,7 +58,6 @@ internal class SwapDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, singleAccountListSupplier: SingleAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, dispatcherProvider: CoroutineDispatcherProvider, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( @@ -67,7 +65,6 @@ internal class SwapDataModule { responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, networkFactory = networkFactory, singleAccountListSupplier = singleAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, dispatchers = dispatcherProvider, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt index a9764ab530..e8d4a35b72 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt @@ -1,13 +1,11 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse import com.tangem.utils.extensions.orZero -import java.math.BigDecimal internal class DefaultInitialToCurrencyResolver( private val swapTransactionRepository: SwapTransactionRepository, @@ -18,22 +16,6 @@ internal class DefaultInitialToCurrencyResolver( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? { - val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null - - return if (id != initialCryptoCurrency.id.value) { - val group = state.getGroupWithReverse(isReverseFromTo) - group.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus - } else { - null - } - } - - override suspend fun tryGetFromCacheV2( - userWallet: UserWallet, - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, ): AccountSwapCurrency? { val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null @@ -48,14 +30,7 @@ internal class DefaultInitialToCurrencyResolver( } } - override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? { - val group = state.getGroupWithReverse(isReverseFromTo) - return group.available.maxByOrNull { - it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO - }?.currencyStatus - } - - override fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? { + override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) return group.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> currencyList.maxByOrNull { swapAccountCurrency -> diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt index 7759a61e98..da6436ee57 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress @@ -13,16 +12,7 @@ interface InitialToCurrencyResolver { initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? - - fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? - - suspend fun tryGetFromCacheV2( - userWallet: UserWallet, - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, ): AccountSwapCurrency? - fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? + fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index c1193cdb4c..98165323aa 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -105,12 +105,6 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - suspend fun getInitialCurrencyToSwap( - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? - /** * Returns initial currency to swap as AccountSwapCurrency * @@ -118,7 +112,7 @@ interface SwapInteractor { * @param state current tokens data state * @param isReverseFromTo flag indicating the direction of the swap */ - suspend fun getInitialCurrencyToSwapV2( + suspend fun getInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index c3793db861..1856211b21 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -19,7 +19,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -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.appcurrency.GetSelectedAppCurrencyUseCase @@ -75,7 +74,6 @@ import java.math.RoundingMode internal class SwapInteractorImpl @AssistedInject constructor( private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, - private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -106,7 +104,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val rampStateManager: RampStateManager, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val walletManagersFacade: WalletManagersFacade, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -123,58 +120,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - return if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyTokensDataState(currency) - } else { - getCurrencyTokensDataState(currency) - } - } - - private suspend fun getCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase - .invokeMultiWalletSync(userWalletId) - .getOrElse { emptyList() } - - val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses - .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 - } - - if (walletCurrencyStatusesExceptInitial.isEmpty()) { - return TokensDataStateExpress.EMPTY - } - - val pairsLeast = getPairs( - userWallet = userWallet, - initialCurrency = LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - currenciesList = walletCurrencyStatusesExceptInitial.map { it.currency }, - ) - - return TokensDataStateExpress( - fromGroup = getToCurrenciesGroup( - currency = currency, - leastPairs = pairsLeast.pairs, - cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.to }, - tokenInfoForAvailable = { it.from }, - ), - toGroup = getToCurrenciesGroup( - currency = currency, - leastPairs = pairsLeast.pairs, - cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.from }, - tokenInfoForAvailable = { it.to }, - ), - allProviders = pairsLeast.allProviders, - ) + return getAccountCurrencyTokensDataState(currency) } private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { @@ -212,14 +158,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) return TokensDataStateExpress( - fromGroup = getToCurrenciesGroupV2( + fromGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial, tokenInfoForFilter = { it.to }, tokenInfoForAvailable = { it.from }, ), - toGroup = getToCurrenciesGroupV2( + toGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial, @@ -242,39 +188,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun getToCurrenciesGroup( - currency: CryptoCurrency, - leastPairs: List, - cryptoCurrenciesList: List, - tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, - tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, - ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { pair -> - tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(pair).network == currency.network.backendId - } - - val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { cryptoCurrencyStatus -> - val providers = findProvidersForPair(cryptoCurrencyStatus, filteredPairs, tokenInfoForAvailable) - if (providers != null) { - CryptoCurrencySwapInfo(cryptoCurrencyStatus, providers) - } else { - null - } - } - - val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies - .map { it.currencyStatus } - .toSet() - - return CurrenciesGroup( - available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, - accountCurrencyList = emptyList(), - isAfterSearch = false, - ) - } - - private suspend fun getToCurrenciesGroupV2( currency: CryptoCurrency, leastPairs: List, cryptoCurrenciesList: Map>, @@ -1323,7 +1236,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? { + ): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) return initialToCurrencyResolver.tryGetFromCache( userWallet = userWallet, @@ -1332,22 +1245,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo = isReverseFromTo, ) ?: initialToCurrencyResolver.tryGetWithMaxAmount(state = state, isReverseFromTo = isReverseFromTo) - ?: group.available.firstOrNull()?.currencyStatus - } - - override suspend fun getInitialCurrencyToSwapV2( - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, - ): AccountSwapCurrency? { - val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCacheV2( - userWallet = userWallet, - initialCryptoCurrency = initialCryptoCurrency, - state = state, - isReverseFromTo = isReverseFromTo, - ) - ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state = state, isReverseFromTo = isReverseFromTo) ?: group.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> accountSwapCurrency.isAvailable diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 5b8d84540a..8a54dbba3d 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { /** Api */ implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) + implementation(projects.features.approval.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 6a88017cb4..033d257c1d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -12,11 +12,15 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen 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.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent @@ -26,8 +30,8 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.ui.SwapScreen import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen +import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import com.tangem.utils.extensions.isZero @@ -41,8 +45,8 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SwapComponent.Params, private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, - private val sendFeatureToggles: SendFeatureToggles, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, + private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { private val model: SwapModel = getOrCreateModel(params) @@ -52,7 +56,22 @@ internal class DefaultSwapComponent @AssistedInject constructor( serializer = AddToPortfolioRoute.serializer(), key = BOTTOM_SHEET_SLOT_KEY, handleBackButton = false, - childFactory = { configuration, context -> bottomSheetChild(context) }, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + + private val approvalSlot = childSlot( + key = APPROVAL_SLOT_KEY, + source = model.approvalSlotNavigation, + serializer = null, + handleBackButton = true, + childFactory = { _, factoryContext -> + val approvalParams = getApprovalParams() + ?: error("Approval params are not available") + giveApprovalComponentFactory.create( + context = childByContext(factoryContext), + params = approvalParams, + ) + }, ) init { @@ -62,8 +81,8 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } - val slotNavigation = SlotNavigation() - val childSlot = childSlot( + private val slotNavigation = SlotNavigation() + private val childSlot = childSlot( source = slotNavigation, serializer = null, key = FEE_SELECTOR_SLOT_KEY, @@ -102,37 +121,35 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable override fun Content(modifier: Modifier) { - if (sendFeatureToggles.isGaslessTransactionsEnabled) { - val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle() - val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } } - val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } } - val shouldHideBlock by remember { - derivedStateOf { toBigDecimalOrZero(dataState.amount).isZero() || model.uiState.isInsufficientFunds } + val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle() + val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } } + val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } } + val shouldHideBlock by remember { + derivedStateOf { toBigDecimalOrZero(dataState.amount).isZero() || model.uiState.isInsufficientFunds } + } + + LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) { + if (shouldHideBlock) { + slotNavigation.dismiss() + return@LaunchedEffect } - LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) { - if (shouldHideBlock) { - slotNavigation.dismiss() - return@LaunchedEffect - } - - val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run { - slotNavigation.dismiss() - return@LaunchedEffect - } - - val feeCurrencyStatus = feePaidCryptoCurrency ?: run { - slotNavigation.dismiss() - return@LaunchedEffect - } - - slotNavigation.activate( - FeeSelectorConfig( - sendingCurrencyStatus = sendingCryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - ), - ) + val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run { + slotNavigation.dismiss() + return@LaunchedEffect } + + val feeCurrencyStatus = feePaidCryptoCurrency ?: run { + slotNavigation.dismiss() + return@LaunchedEffect + } + + slotNavigation.activate( + FeeSelectorConfig( + sendingCurrencyStatus = sendingCryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ), + ) } val feeSelectorChildStackState by childSlot.subscribeAsState() @@ -191,6 +208,9 @@ internal class DefaultSwapComponent @AssistedInject constructor( } bottomSheet.child?.instance?.BottomSheet() + + val approvalSlotState by approvalSlot.subscribeAsState() + approvalSlotState.child?.instance?.BottomSheet() } @Suppress("UnsafeCallOnNullableType") @@ -205,6 +225,27 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } + fun getApprovalParams(): GiveApprovalComponent.Params? { + val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest + ?: return null + val fromCryptoCurrency = model.dataState.fromCryptoCurrency ?: return null + val feeCryptoCurrency = model.dataState.feePaidCryptoCurrency ?: return null + val providerName = model.dataState.selectedProvider?.name.orEmpty() + + return GiveApprovalComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = fromCryptoCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrency, + amount = permissionState.amount, + spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress, + subtitle = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, permissionState.currency), + ), + callback = model.approvalCallback, + ) + } + private fun toBigDecimalOrZero(bigDecimalString: String?): BigDecimal { return bigDecimalString?.replace(",", ".")?.toBigDecimalOrNull() ?: BigDecimal.ZERO } @@ -217,5 +258,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( private companion object { const val BOTTOM_SHEET_SLOT_KEY = "bottomSheetSlot" const val FEE_SELECTOR_SLOT_KEY = "feeSelectorSlot" + const val APPROVAL_SLOT_KEY = "approvalSlot" } } \ No newline at end of file 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 919e908498..915211edc6 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 @@ -3,6 +3,7 @@ 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.account.TokensListPortfolioItemConverter import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -27,7 +28,7 @@ internal class AccountTokenItemConverter( ) : Converter { override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio { - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, @@ -39,7 +40,7 @@ internal class AccountTokenItemConverter( createAvailableItemConverter() .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), - ) + ).convert(Unit) } fun createAvailableItemConverter(): TokenItemStateConverter { 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 dc6c9cfbb0..653bb2b5e3 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 @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ui.SwapTransactionState import com.tangem.feature.swap.models.SwapAlertUM @@ -11,24 +11,25 @@ import com.tangem.utils.converter.Converter internal class SwapTransactionErrorStateConverter( private val onDismiss: () -> Unit, private val onSupportClick: (String) -> Unit, -) : Converter { - override fun convert(value: SwapTransactionState.Error): AlertUM? { + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), +) : Converter { + override fun convert(value: SwapTransactionState.Error): DialogMessage? { return when (value) { is SwapTransactionState.Error.TransactionError -> { when (val error = value.error) { is SendTransactionError.UserCancelledError -> return null - null -> SwapAlertUM.DefaultError(onDismiss) - else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) + null -> SwapAlertUM.genericError(onDismiss) + else -> transactionErrorDialogFactory.create(error, onDismiss, onSupportClick) } } is SwapTransactionState.Error.ExpressError -> { - SwapAlertUM.ExpressErrorAlert( + SwapAlertUM.expressErrorAlert( message = getExpressErrorMessage(value.error), onConfirmClick = { onSupportClick(value.error.code.toString()) }, ) } - SwapTransactionState.Error.UnknownError -> SwapAlertUM.DefaultError(onDismiss) - is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.SupportError( + SwapTransactionState.Error.UnknownError -> SwapAlertUM.genericError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.genericError( 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 791af35a8c..d54ae4ecc1 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 @@ -1,113 +1,80 @@ package com.tangem.feature.swap.converters -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo -import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData +import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.TokenListUMData -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 com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.plus +import kotlinx.collections.immutable.toPersistentList internal class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, - private val isBalanceHiddenProvider: Provider, - private val appCurrencyProvider: Provider, -) : Converter { + private val tokensDataState: CurrenciesGroup, + private val isBalanceHidden: Boolean, + private val isAccountsMode: Boolean, + appCurrencyProvider: Provider, +) : Transformer { - override fun convert(value: CurrenciesGroupWithFromCurrency): SwapSelectTokenStateHolder { - val group = value.group - val availableTitle = TokenToSelectState.Title( - resourceReference(R.string.exchange_tokens_available_tokens_header), - ) - val allTokens = group.available + group.unavailable - return SwapSelectTokenStateHolder( - availableTokens = allTokens.map { tokenWithBalanceToTokenToSelect(it, true) } - .toMutableList() - .apply { - if (this.isNotEmpty()) { - this.add(0, availableTitle) + private val accountListItemConverter = AccountTokenItemConverter( + appCurrency = appCurrencyProvider(), + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + onItemClick = onTokenSelected, + ) + + override fun transform(prevState: SwapStateHolder): SwapStateHolder { + val accountList = tokensDataState.accountCurrencyList + val currentMarketsState = prevState.selectTokenState?.marketsState + return prevState.copy( + selectTokenState = SwapSelectTokenStateHolder( + availableTokens = persistentListOf(), + unavailableTokens = persistentListOf(), + tokensListData = if (isAccountsMode) { + val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() + val totalTokensCount = portfolioList.sumOf { it.tokens.size } + if (totalTokensCount > 0) { + TokenListUMData.AccountList( + tokensList = portfolioList, + totalTokensCount = totalTokensCount, + ) + } else { + TokenListUMData.EmptyList } - } - .toImmutableList(), - unavailableTokens = persistentListOf(), - tokensListData = TokenListUMData.EmptyList, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHiddenProvider(), - isAfterSearch = group.isAfterSearch, - ) - } + } else { + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { accountSwapCurrency -> + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() - private fun tokenWithBalanceToTokenToSelect( - cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, - isAvailable: Boolean, - ): TokenToSelectState { - val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus - return TokenToSelectState.TokenToSelect( - id = cryptoCurrencyStatus.currency.id.value, - name = cryptoCurrencyStatus.currency.name, - symbol = cryptoCurrencyStatus.currency.symbol, - isAvailable = isAvailable, - tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), - addedTokenBalanceData = TokenBalanceData( - amount = formatCryptoAmount(cryptoCurrencyStatus), - amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), - isBalanceHidden = isBalanceHiddenProvider.invoke(), + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + totalTokensCount = tokensList.size, + ) + } else { + TokenListUMData.EmptyList + } + }, + marketsState = currentMarketsState, + onSearchEntered = onSearchEntered, + onTokenSelected = onTokenSelected, + isBalanceHidden = isBalanceHidden, + isAfterSearch = tokensDataState.isAfterSearch, ), ) } - - private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): CurrencyIconState { - return when (currency) { - is CryptoCurrency.Coin -> { - CurrencyIconState.CoinIcon( - url = currency.iconUrl, - fallbackResId = currency.networkIconResId, - isGrayscale = !isAvailable, - shouldShowCustomBadge = currency.isCustom, - ) - } - is CryptoCurrency.Token -> { - val isGrayscale = currency.network.isTestnet - val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) - val tint = getTintForTokenIcon(background) - CurrencyIconState.TokenIcon( - url = currency.iconUrl, - isGrayscale = !isAvailable, - shouldShowCustomBadge = currency.isCustom, - topBadgeIconResId = currency.networkIconResId, - fallbackTint = tint, - fallbackBackground = background, - ) - } - } - } - - private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { - return cryptoCurrencyStatus.value.amount.format { - crypto(cryptoCurrencyStatus.currency) - } - } - - private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return cryptoCurrencyStatus.value.fiatAmount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } - } } \ No newline at end of file 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 deleted file mode 100644 index 668b30aa88..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup -import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Provider -import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.plus -import kotlinx.collections.immutable.toPersistentList - -internal class TokensDataConverterV2( - private val onSearchEntered: (String) -> Unit, - private val onTokenSelected: (String) -> Unit, - private val tokensDataState: CurrenciesGroup, - private val isBalanceHidden: Boolean, - private val isAccountsMode: Boolean, - appCurrencyProvider: Provider, -) : Transformer { - - private val accountListItemConverter = AccountTokenItemConverter( - appCurrency = appCurrencyProvider(), - unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), - onItemClick = onTokenSelected, - ) - - override fun transform(prevState: SwapStateHolder): SwapStateHolder { - val accountList = tokensDataState.accountCurrencyList - val currentMarketsState = prevState.selectTokenState?.marketsState - return prevState.copy( - selectTokenState = SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), - tokensListData = if (isAccountsMode) { - val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() - val totalTokensCount = portfolioList.sumOf { it.tokens.size } - TokenListUMData.AccountList( - tokensList = portfolioList, - totalTokensCount = totalTokensCount, - ) - } else { - val tokensList = accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() - .convert(accountSwapCurrency.cryptoCurrencyStatus) - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList() - - if (tokensList.isNotEmpty()) { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + tokensList, - totalTokensCount = tokensList.size, - ) - } else { - TokenListUMData.EmptyList - } - }, - marketsState = currentMarketsState, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHidden, - isAfterSearch = tokensDataState.isAfterSearch, - ), - ) - } -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index ab7b88c30d..4c90fa3f34 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 @@ -24,14 +24,16 @@ 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.decompose.ui.UiMessageSender 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.extensions.* +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -78,6 +80,7 @@ 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.SwapTransactionErrorStateConverter import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.TransactionFeeResult import com.tangem.feature.swap.domain.TxFeeSealedState @@ -87,6 +90,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager @@ -96,9 +100,10 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.SwapComponent @@ -148,12 +153,10 @@ internal class SwapModel @Inject constructor( router: AppRouter, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, - private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, @@ -162,6 +165,8 @@ internal class SwapModel @Inject constructor( private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, + private val messageSender: UiMessageSender, + giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -225,7 +230,7 @@ internal class SwapModel @Inject constructor( val feeSelectorRepository = FeeSelectorRepository() // shows currency order (direct - swap initial to selected, reversed = selected to initial) - var isOrderReversed by mutableStateOf(false) + private var isOrderReversed by mutableStateOf(false) private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) private val swapRouter: SwapRouter = SwapRouter(router = router) @@ -235,12 +240,12 @@ internal class SwapModel @Inject constructor( private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null /** - * If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null - * If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null + * If user came from Tangem Pay -> fromAccountCurrencyStatus == null + * If user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null * * Remove when accounts are integrated into Tangem Pay */ - private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null + private val canUseFromAccountCurrencyStatus = tangemPayInput == null private val isUserResolvableError: (SwapState) -> Boolean = { swapState -> swapState is SwapState.SwapError && @@ -290,6 +295,33 @@ internal class SwapModel @Inject constructor( get() = swapRouter.currentScreen val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val approvalSlotNavigation = SlotNavigation() + private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled + + val approvalCallback = object : GiveApprovalComponent.Callback { + override fun onApproveClick() { + sendPermissionApproveClickedEvent() + } + + override fun onApproveDone() { + approvalSlotNavigation.dismiss() + updateWalletBalance() + uiState = stateBuilder.loadingPermissionState(uiState) + startLoadingQuotesFromLastState(isSilent = true) + } + + override fun onApproveFailed() { + approvalSlotNavigation.dismiss() + showAlert() + } + + override fun onCancelClick() { + approvalSlotNavigation.dismiss() + startLoadingQuotesFromLastState(isSilent = true) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) + } + } + val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { override fun onDismiss() = bottomSheetNavigation.dismiss() @@ -342,7 +374,8 @@ internal class SwapModel @Inject constructor( } if (fromAccountStatus == null) { - uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { fromAccountCurrencyStatus = fromAccountStatus toAccountCurrencyStatus = toAccountStatus @@ -360,7 +393,8 @@ internal class SwapModel @Inject constructor( } if (fromStatus == null) { - uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { initialFromStatus = fromStatus initialToStatus = toStatus @@ -476,25 +510,25 @@ internal class SwapModel @Inject constructor( }.onSuccess { state -> updateTokensState(state) - val (selectedCurrency, selectedAccount) = if (accountsFeatureToggles.isFeatureEnabled) { - val selectedAccountCurrency = toAccountCurrencyStatus ?: swapInteractor.getInitialCurrencyToSwapV2( - initialCryptoCurrency = initialCurrencyFrom, - state = state, - isReverseFromTo = isReverseFromTo, - )?.let { accountSwapCurrency -> - AccountCryptoCurrencyStatus( - account = accountSwapCurrency.account, - status = accountSwapCurrency.cryptoCurrencyStatus, + val (selectedCurrency, selectedAccount) = run { + var selectedAccountCurrency = toAccountCurrencyStatus + + if (selectedAccountCurrency == null) { + val amountSwapCurrency = swapInteractor.getInitialCurrencyToSwap( + initialCryptoCurrency = initialCurrencyFrom, + state = state, + isReverseFromTo = isReverseFromTo, ) + + if (amountSwapCurrency != null) { + selectedAccountCurrency = AccountCryptoCurrencyStatus( + account = amountSwapCurrency.account, + status = amountSwapCurrency.cryptoCurrencyStatus, + ) + } } + selectedAccountCurrency?.status to selectedAccountCurrency?.account - } else { - val selectedCurrency = initialToStatus ?: swapInteractor.getInitialCurrencyToSwap( - initialCryptoCurrency = initialCurrencyFrom, - state = state, - isReverseFromTo = isReverseFromTo, - ) - selectedCurrency to null } applyInitialTokenChoice( @@ -657,19 +691,11 @@ internal class SwapModel @Inject constructor( private fun updateTokensState(tokenDataState: TokensDataStateExpress) { val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup - uiState = if (accountsFeatureToggles.isFeatureEnabled) { - stateBuilder.addTokensToStateV2( - uiState = uiState, - tokensDataState = tokensDataState, - isAccountsMode = isAccountsMode, - ) - } else { - stateBuilder.addTokensToState( - uiState = uiState, - tokensDataState = tokensDataState, - fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, - ) - } + uiState = stateBuilder.addTokensToStateV2( + uiState = uiState, + tokensDataState = tokensDataState, + isAccountsMode = isAccountsMode, + ) latestMarketsState?.let(::applyMarketsState) } @@ -1020,7 +1046,7 @@ internal class SwapModel @Inject constructor( val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) startLoadingQuotesFromLastState() @@ -1046,7 +1072,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } sendSuccessSwapEvent( @@ -1090,21 +1116,11 @@ internal class SwapModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.Error -> { startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) @@ -1113,7 +1129,7 @@ internal class SwapModel @Inject constructor( }.onFailure { error -> Timber.e(error) startLoadingQuotesFromLastState() - makeDefaultAlert() + showAlert() } } } @@ -1205,7 +1221,7 @@ internal class SwapModel @Inject constructor( } val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { - makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") return@launch } @@ -1221,7 +1237,7 @@ internal class SwapModel @Inject constructor( fromTokenStatus = fromCryptoCurrency, approveType = approveType, txFee = feeForPermission, - spenderAddress = approveDataModel.spenderAddress, + spenderAddress = requireNotNull(dataState.approveDataModel).spenderAddress, ), ) }.onSuccess { swapTransactionState -> @@ -1235,29 +1251,19 @@ internal class SwapModel @Inject constructor( startLoadingQuotesFromLastState(isSilent = true) } is SwapTransactionState.Error -> { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) } } - }.onFailure { makeDefaultAlert() } + }.onFailure { showAlert() } }.onFailure { error -> Timber.e(error.message.orEmpty()) - makeDefaultAlert() + showAlert() } } } @@ -1348,11 +1354,7 @@ internal class SwapModel @Inject constructor( fromToken = foundToken fromAccount = foundAccount toToken = initialFromStatus - toAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus?.account - } else { - null - } + toAccount = fromAccountCurrencyStatus?.account val newToken = fromToken.currency as? CryptoCurrency.Coin if (newToken != null) { @@ -1366,11 +1368,7 @@ internal class SwapModel @Inject constructor( } } else { fromToken = initialFromStatus - fromAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus?.account - } else { - null - } + fromAccount = fromAccountCurrencyStatus?.account toToken = foundToken toAccount = foundAccount @@ -1428,26 +1426,16 @@ internal class SwapModel @Inject constructor( tokens: TokensDataStateExpress, id: String, ): Pair { - return if (accountsFeatureToggles.isFeatureEnabled) { - val accountCryptoCurrencyStatus = if (isOrderReversed) { - tokens.fromGroup - } else { - tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> - accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id - } - } - accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account + val accountCryptoCurrencyStatus = if (isOrderReversed) { + tokens.fromGroup } else { - if (isOrderReversed) { - tokens.fromGroup - } else { - tokens.toGroup - }.available.firstOrNull { swapAvailability -> - swapAvailability.currencyStatus.currency.id.value == id - }?.currencyStatus to null + tokens.toGroup + }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + } } + return accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1458,79 +1446,44 @@ internal class SwapModel @Inject constructor( ) { Timber.d("Subscribe to ${coin.id} balance updates") - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId = userWalletId, - currency = coin, - ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes - .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") + getAccountCurrencyStatusUseCase( + userWalletId = userWalletId, + currency = coin, + ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes + .onEach { (account, currencyStatus) -> + Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") - if (isFromCurrency) { - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = currencyStatus, - ).getOrNull() ?: currencyStatus, - ) - } - - uiState = when { - isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { - dataState = dataState.copy( - fromCryptoCurrency = currencyStatus, - fromAccount = account, - ) - stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) - } - !isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> { - dataState = dataState.copy( - toCryptoCurrency = currencyStatus, - toAccount = account, - ) - stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) - } - else -> { - uiState - } - } - startLoadingQuotesFromLastState(isSilent = true) + if (isFromCurrency) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = currencyStatus, + ).getOrNull() ?: currencyStatus, + ) } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = coin.id, - isSingleWalletWithTokens = false, - ).mapNotNull { either -> (either as? Either.Right)?.value } - .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes - .onEach { status -> - Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}") - if (isFromCurrency) { + uiState = when { + isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = status, - ).getOrNull() ?: status, + fromCryptoCurrency = currencyStatus, + fromAccount = account, ) + stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) } - - uiState = when { - isFromCurrency && status.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { - dataState = dataState.copy(fromCryptoCurrency = status) - stateBuilder.updateSendCurrencyBalance(uiState, status) - } - !isFromCurrency && status.currency.id == dataState.toCryptoCurrency?.currency?.id -> { - dataState = dataState.copy(toCryptoCurrency = status) - stateBuilder.updateReceiveCurrencyBalance(uiState, status) - } - else -> { - uiState - } + !isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> { + dataState = dataState.copy( + toCryptoCurrency = currencyStatus, + toAccount = account, + ) + stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) + } + else -> { + uiState } - startLoadingQuotesFromLastState(isSilent = true) } - }.flowOn(dispatchers.main) + startLoadingQuotesFromLastState(isSilent = true) + } + .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder) } @@ -1657,18 +1610,97 @@ internal class SwapModel @Inject constructor( return inputNumberFormatter.getValidatedNumberWithFixedDecimals(amount, maxDecimals) } - private fun makeDefaultAlert() { - uiState = stateBuilder.addDefaultAlert(uiState = uiState) + private fun showAlert(message: TextReference = resourceReference(R.string.common_unknown_error)) { + messageSender.send(SwapAlertUM.genericError(onConfirmClick = { }, message = message)) } - private fun makeSupportAlert(message: TextReference) { - uiState = stateBuilder.addSupportAlert( - uiState = uiState, - message = message, - onSupportClick = { onFailedTxEmailClick("Fee calculation error") }, + private fun showDemoModeAlert() { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = {}, + ), + ), ) } + private fun showTransactionErrorAlert( + error: SwapTransactionState.Error, + onSupportClick: (String) -> Unit = ::onFailedTxEmailClick, + ) { + val errorAlert = SwapTransactionErrorStateConverter( + onDismiss = {}, + onSupportClick = onSupportClick, + ).convert(error) + errorAlert?.let { messageSender.send(it) } + } + + private fun onTangemPaySupportClick(txId: String) { + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull().orEmpty() + val email = FeedbackEmailType.Visa.Withdrawal( + walletMetaInfo = metaInfo, + customerId = customerId, + providerName = dataState.selectedProvider?.name.orEmpty(), + txId = txId, + ) + analyticsEventHandler.send(Basic.ButtonSupport(source = ScreensSources.Swap)) + sendFeedbackEmailUseCase(email) + } + } + + private fun showSwapInfoAlert(isPriceImpact: Boolean, token: String, provider: SwapProvider) { + messageSender.send( + SwapAlertUM.informationAlert( + message = buildSwapInfoMessage(isPriceImpact, token, provider), + onConfirmClick = {}, + ), + ) + } + + private fun buildSwapInfoMessage(isPriceImpact: Boolean, token: String, provider: SwapProvider): TextReference { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}%" } + val messages = buildList { + when (provider.type) { + ExchangeProviderType.CEX -> { + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + } + } + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> { + if (isPriceImpact) { + add(resourceReference(R.string.swapping_high_price_impact_description)) + add(stringReference("\n\n")) + } + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) + } + } + } + } + return combinedReference(messages.toWrappedList()) + } + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun createUiActions(): UiActions { return UiActions( @@ -1714,10 +1746,14 @@ internal class SwapModel @Inject constructor( openPermissionBottomSheet = { singleTaskScheduler.cancelTask() sendGivePermissionClickedEvent() - uiState = stateBuilder.showPermissionBottomSheet(uiState) { - startLoadingQuotesFromLastState(isSilent = true) - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) - uiState = stateBuilder.dismissBottomSheet(uiState) + if (shouldUseGaslessApproval) { + approvalSlotNavigation.activate(Unit) + } else { + uiState = stateBuilder.showPermissionBottomSheet(uiState) { + startLoadingQuotesFromLastState(isSilent = true) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) + uiState = stateBuilder.dismissBottomSheet(uiState) + } } }, onAmountSelected = { onAmountSelected(it) }, @@ -1786,14 +1822,7 @@ internal class SwapModel @Inject constructor( val selectedProvider = dataState.selectedProvider ?: return@UiActions val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions val isPriceImpact = uiState.priceImpact is PriceImpact.Value - uiState = stateBuilder.createAlert( - uiState = uiState, - isPriceImpact = isPriceImpact, - token = currencySymbol, - provider = selectedProvider, - isReverseSwapPossible = isReverseSwapPossible(), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - ) + showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider) }, onLinkClick = urlOpener::openUrl, onSelectTokenClick = { @@ -1934,18 +1963,12 @@ internal class SwapModel @Inject constructor( toToken.currency.id.value } - return if (accountsFeatureToggles.isFeatureEnabled) { - groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> - currencyList.find { accountSwapCurrency -> - idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && - accountSwapCurrency.isAvailable - } - }?.providers - } else { - groupToFind.available.find { swapAvailability -> - idToFind == swapAvailability.currencyStatus.currency.id.value - }?.providers - } + return groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> + currencyList.find { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + }?.providers ?.filterForTangemPayWithdrawal() .orEmpty() } @@ -1989,16 +2012,10 @@ internal class SwapModel @Inject constructor( 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 + return group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable } } } @@ -2035,14 +2052,10 @@ internal class SwapModel @Inject constructor( val chosen = if (isOrderReversed) from else to - return if (accountsFeatureToggles.isFeatureEnabled) { - currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> - accountSwapAvailability.currencyList.map { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus - } + return currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> + accountSwapAvailability.currencyList.map { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus } - } else { - currenciesGroup.available.map { swapAvailability -> swapAvailability.currencyStatus } }.map { currencyStatus -> currencyStatus.currency }.contains(chosen.currency) } @@ -2130,31 +2143,12 @@ internal class SwapModel @Inject constructor( } private fun onTangemPayWithdrawalError(txId: String?) { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, + showTransactionErrorAlert( error = SwapTransactionState.Error.TangemPayWithdrawalError(txId.orEmpty()), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = { - val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: "Unknown" - onTangemPaySupportClick(customerId = customerId, txId = txId) - }, - isReverseSwapPossible = isReverseSwapPossible(), + onSupportClick = ::onTangemPaySupportClick, ) } - private fun onTangemPaySupportClick(customerId: String, txId: String?) { - modelScope.launch { - val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch - val email = FeedbackEmailType.Visa.Withdrawal( - walletMetaInfo = metaInfo, - customerId = customerId, - providerName = dataState.selectedProvider?.name.orEmpty(), - txId = txId.orEmpty(), - ) - sendFeedbackEmailUseCase(email) - } - } - private fun onFailedTxEmailClick(errorMessage: String) { modelScope.launch { val transaction = dataState.swapDataModel?.transaction @@ -2186,6 +2180,7 @@ internal class SwapModel @Inject constructor( txId = transaction?.txId.orEmpty(), ) + analyticsEventHandler.send(Basic.ButtonSupport(source = ScreensSources.Swap)) sendFeedbackEmailUseCase(email) } } @@ -2264,6 +2259,8 @@ internal class SwapModel @Inject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, @@ -2316,13 +2313,6 @@ internal class SwapModel @Inject constructor( } private fun getSelectedFeeState(): TxFeeSealedState { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return TxFeeSealedState.Legacy( - txFeeState = TxFeeState.Empty, - selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ) - } - val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return TxFeeSealedState.Legacy( txFeeState = TxFeeState.Empty, @@ -2341,10 +2331,6 @@ internal class SwapModel @Inject constructor( } private fun getSelectedFee(): TxFee? { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return dataState.selectedFee - } - val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return null val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended 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 bb7763f928..fcf6277b98 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 @@ -1,47 +1,43 @@ package com.tangem.feature.swap.models -import com.tangem.common.ui.alerts.models.AlertUM 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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction -sealed class SwapAlertUM : AlertUM { +internal object SwapAlertUM { - 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) - } + fun genericError( + onConfirmClick: () -> Unit, + message: TextReference = resourceReference(R.string.common_unknown_error), + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class SupportError( - 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_support) - } + fun expressErrorAlert( + message: TextReference = resourceReference(R.string.common_unknown_error), + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class ExpressErrorAlert( - override val message: TextReference = resourceReference(R.string.common_unknown_error), - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference? = null - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } - - data class InformationAlert( - override val message: TextReference, - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference = resourceReference( - R.string.swapping_alert_title, - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_ok) - } + fun informationAlert(message: TextReference, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.swapping_alert_title), + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 47517e2801..02b46b544c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -7,14 +7,11 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState -import com.tangem.feature.swap.models.states.events.SwapEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +21,6 @@ internal data class SwapStateHolder( val blockchainId: String, // not the same as networkId, its local id in app val notifications: ImmutableList = persistentListOf(), val isInsufficientFunds: Boolean, - val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState, val providerState: ProviderState, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt deleted file mode 100644 index a00fed2610..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.swap.models.states.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM - -@Immutable -internal sealed class SwapEvent { - data class ShowAlert(val alert: AlertUM) : SwapEvent() -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6e0f5f520a..3f8b76b556 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 @@ -5,33 +5,27 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter -import com.tangem.feature.swap.converters.TokensDataConverterV2 import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType @@ -43,12 +37,10 @@ import com.tangem.feature.swap.model.SwapNotificationsFactory import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* -import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -69,7 +61,7 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, private val isAccountsModeProvider: Provider, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, - private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, + holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, ) { private val isHoldToConfirmEnabled: Boolean = @@ -77,13 +69,6 @@ internal class StateBuilder( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val tokensDataConverter = TokensDataConverter( - onSearchEntered = actions.onSearchEntered, - onTokenSelected = actions.onTokenSelected, - isBalanceHiddenProvider = isBalanceHiddenProvider, - appCurrencyProvider = appCurrencyProvider, - ) - private val notificationsFactory by lazy(LazyThreadSafetyMode.NONE) { SwapNotificationsFactory(actions, iGaslessFeeSupportedForNetwork) } @@ -684,28 +669,12 @@ internal class StateBuilder( ) } - fun addTokensToState( - uiState: SwapStateHolder, - fromToken: CryptoCurrency, - tokensDataState: CurrenciesGroup, - ): SwapStateHolder { - val currentMarketsState = uiState.selectTokenState?.marketsState - return uiState.copy( - selectTokenState = tokensDataConverter.convert( - value = CurrenciesGroupWithFromCurrency( - fromCurrency = fromToken, - group = tokensDataState, - ), - ).copy(marketsState = currentMarketsState), - ) - } - fun addTokensToStateV2( uiState: SwapStateHolder, tokensDataState: CurrenciesGroup, isAccountsMode: Boolean, ): SwapStateHolder { - return TokensDataConverterV2( + return TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, appCurrencyProvider = appCurrencyProvider, @@ -718,6 +687,9 @@ internal class StateBuilder( fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, + notifications = uiState.notifications + .filterNot { it is SwapNotificationUM.Info.PermissionNeeded } + .toImmutableList(), ) } @@ -975,133 +947,6 @@ internal class StateBuilder( ) } - fun createErrorTransactionAlert( - uiState: SwapStateHolder, - error: SwapTransactionState.Error, - onDismiss: () -> Unit, - onSupportClick: (String) -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val errorAlert = SwapTransactionErrorStateConverter( - onSupportClick = onSupportClick, - onDismiss = onDismiss, - ).convert(error) - return uiState.copy( - event = errorAlert?.let { - triggeredEvent( - data = SwapEvent.ShowAlert(errorAlert), - onConsume = onDismiss, - ) - } ?: consumedEvent(), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun createDemoModeAlert( - uiState: SwapStateHolder, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - @Suppress("LongParameterList") - fun createAlert( - uiState: SwapStateHolder, - isPriceImpact: Boolean, - token: String, - provider: SwapProvider, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } - val combinedMessage = buildList { - when (provider.type) { - ExchangeProviderType.CEX -> { - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_cex_description_with_slippage, - formatArgs = wrappedList(token, slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) - } - } - ExchangeProviderType.DEX, - ExchangeProviderType.DEX_BRIDGE, - -> { - if (isPriceImpact) { - add(resourceReference(R.string.swapping_high_price_impact_description)) - add(stringReference("\n\n")) - } - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_dex_description_with_slippage, - formatArgs = wrappedList(slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) - } - } - } - } - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - SwapAlertUM.InformationAlert( - message = combinedReference(combinedMessage.toWrappedList()), - onConfirmClick = onDismiss, - ), - ), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun addDefaultAlert( - uiState: SwapStateHolder, - message: TextReference = resourceReference(R.string.common_unknown_error), - onDismiss: () -> Unit = { clearAlert(uiState) }, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - 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, - ), - ) - } - - fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) - fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { return uiState.copy( notifications = notificationsFactory.getGeneralErrorStateNotifications( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt deleted file mode 100644 index 3a7455b5b0..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.feature.swap.ui - -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.feature.swap.models.states.events.SwapEvent -import com.tangem.feature.swap.presentation.R - -@Composable -internal fun SwapEventEffect(event: StateEvent) { - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - SwapAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is SwapEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - state.onConfirmClick?.invoke() - onDismiss() - }, - ) - val dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file 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 b4d3c15097..813e71cc11 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 @@ -117,10 +117,6 @@ internal fun SwapScreenContent( textAlign = TextAlign.Start, ) } - - SwapEventEffect( - event = state.event, - ) } } 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 25aac0f046..b29e4b8257 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 @@ -328,6 +328,9 @@ private fun Content( ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag( + SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING, + ), ) } else { AnimatedContent(targetState = amountEquivalent, label = "") { amount -> @@ -355,7 +358,9 @@ private fun Content( } else { TangemTheme.colors.text.tertiary }, - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier + .align(Alignment.CenterVertically) + .testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON), ) } } diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt index 393e589bce..0f2ae969eb 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -1,5 +1,5 @@ package com.tangem.features.tangempay interface TangemPayFeatureToggles { - val isTangemPayEnabled: Boolean + val isTangemPayAccountsRefactorEnabled: Boolean } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt index a51c11a3bc..30897aff29 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -5,6 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultTangemPayFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TangemPayFeatureToggles { - override val isTangemPayEnabled - get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") + override val isTangemPayAccountsRefactorEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED") } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 2b5736147e..58f5d9ef84 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -45,7 +45,6 @@ internal class TangemPayDetailsComponent( appComponentContext = child("txHistoryComponent"), params = DefaultTangemPayTxHistoryComponent.Params( userWalletId = params.userWalletId, - customerWalletAddress = params.config.customerWalletAddress, uiActions = model, ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt index bfd618dfa5..d27e63ad51 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -25,7 +25,6 @@ internal class DefaultTangemPayTxHistoryComponent( data class Params( val userWalletId: UserWalletId, - val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions, ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index beb2bed482..4ada6b4b01 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 @@ -7,6 +7,7 @@ import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -124,7 +125,6 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } - subscribeToWithdrawOrder() } fun onPause() { @@ -149,14 +149,6 @@ 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) { @@ -380,6 +372,7 @@ internal class TangemPayDetailsModel @Inject constructor( override fun onContactSupportClicked() { analytics.send(TangemPayAnalyticsEvents.GoToSupportOnBetaBannerClicked()) + analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay)) modelScope.launch { sendFeedbackEmailUseCase.invoke( type = FeedbackEmailType.Visa.FeatureIsBeta( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt index 4e815c1f1a..9569233780 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt @@ -2,6 +2,8 @@ package com.tangem.features.tangempay.model import androidx.compose.runtime.Stable import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -66,6 +68,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor( private fun dispute(customerId: String) { analytics.send(TangemPayAnalyticsEvents.SupportOnTransactionPopupClicked()) + analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay)) modelScope.launch { val walletMetaInfo = getWalletMetaInfoUseCase.invoke(params.userWalletId).getOrNull() ?: return@launch diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index e1433f4d17..47b229ce5d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -31,7 +31,6 @@ internal class TangemPayTxHistoryModel @Inject constructor( private val listManager = TangemPayTxHistoryListManager( repository = tangemPayTxHistoryRepository, dispatchers = dispatchers, - customerWalletAddress = params.customerWalletAddress, txHistoryUiActions = params.uiActions, ) @@ -104,7 +103,7 @@ internal class TangemPayTxHistoryModel @Inject constructor( } private fun loadMoreItems(): Boolean { - modelScope.launch { listManager.loadMore(params.customerWalletAddress) } + modelScope.launch { listManager.loadMore() } return true } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt index 4a1de8ef38..5c7945309d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt @@ -115,13 +115,17 @@ internal object TangemPayTxHistoryDetailsConverter : this.amount.isPositive() -> StringsSigns.MINUS else -> StringsSigns.PLUS } - val amount = this.amount.abs().format { + val amount = when (this.status) { + TangemPayTxHistoryItem.Status.DECLINED -> this.authorizedAmount + else -> this.amount + } + val formattedAmount = amount.abs().format { fiat( fiatCurrencyCode = this@extractAmount.currency.currencyCode, fiatCurrencySymbol = this@extractAmount.currency.symbol, ) } - amountPrefix + amount + amountPrefix + formattedAmount } is TangemPayTxHistoryItem.Collateral -> { val amountPrefix = when { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt index e868de30a5..7ce0c8be84 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt @@ -41,13 +41,17 @@ internal class TangemPayTxHistoryItemsConverter( spend.status == TangemPayTxHistoryItem.Status.DECLINED || spend.amount.isPositive() -> StringsSigns.MINUS else -> StringsSigns.PLUS } - val amount = amountPrefix + spend.amount.abs().format { + val amount = when (spend.status) { + TangemPayTxHistoryItem.Status.DECLINED -> spend.authorizedAmount + else -> spend.amount + } + val formattedAmount = amountPrefix + amount.abs().format { fiat(fiatCurrencyCode = spend.currency.currencyCode, fiatCurrencySymbol = spend.currency.symbol) } return TangemPayTransactionState.Content.Spend( id = spend.id, onClick = { txHistoryUiActions.onTransactionClick(spend) }, - amount = amount, + amount = formattedAmount, amountColor = themedColor { when { spend.status == TangemPayTxHistoryItem.Status.DECLINED -> TangemTheme.colors.text.warning diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index f1b971367c..fa798e16bc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -22,7 +22,6 @@ private typealias TangemPayTxHistoryBatchAction = BatchAction/ +│ ├── Build.kt ← creates the StoryPageFactory for this page +│ └── Story.kt ← the Composable that renders the showcase +├── ui/ +│ ├── StoryBookListScreen.kt ← list of all stories (add your entry here) +│ └── StoryBookScreen.kt ← routes currentPage → correct Composable +└── viewmodel/ + ├── StoryBookViewModel.kt + └── StateUpdater.kt ← helper for stateful pages +``` + +--- + +## Step-by-step: adding a new page + +### 1. Declare the page type in `StoryBookPage.kt` + +For a **stateless** showcase (no user interaction that mutates page state): +```kotlin +internal data object FooStory : StoryBookPage +``` + +For a **stateful** page (e.g. toggle between variants like NorthernLightsStory): +```kotlin +internal data class FooStory( + val selectedVariant: Variant, + val onVariantChange: (Variant) -> Unit, +) : StoryBookPage { + enum class Variant { A, B } +} +``` + +--- + +### 2. Create `page/foo/Build.kt` + +**Stateless:** +```kotlin +internal val fooStoryFactory: StoryPageFactory = StoryPageFactory { FooStory } +``` + +**Stateful** (use `storyPageFactory` + `StateUpdater`): +```kotlin +internal fun StateUpdater.build(): FooStory { + return FooStory( + selectedVariant = FooStory.Variant.A, + onVariantChange = { newVariant -> + updateStory { it.copy(selectedVariant = newVariant) } + }, + ) +} + +internal val fooStoryFactory + get() = storyPageFactory(StateUpdater::build) +``` + +--- + +### 3. Create `page/foo/FooStory.kt` + +Write a `@Composable internal fun FooStory(...)` that renders the showcase. +See [Design guidelines](#design-guidelines) below for layout advice. + +**Stateless example skeleton:** +```kotlin +@Composable +internal fun FooStory(modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier.fillMaxSize()) { + item("section_a") { /* ... */ } + } +} +``` + +**Stateful example skeleton:** +```kotlin +@Composable +internal fun FooStory(state: FooStory, modifier: Modifier = Modifier) { + // use state.selectedVariant, state.onVariantChange +} +``` + +--- + +### 4. Register in `StoryBookScreen.kt` + +Add a branch to the `when` block. + +> **Naming note:** the entity type and the Composable function will share the +> same simple name (e.g. `FooStory`). Kotlin resolves them correctly — the +> entity import is used in the pattern position, the function import is used +> as a call. This is the same pattern used for `NorthernLightsStory` and +> `ButtonsStory`. + +```kotlin +import com.tangem.feature.tester.presentation.storybook.entity.FooStory +import com.tangem.feature.tester.presentation.storybook.page.foo.FooStory + +when (storyState) { + StoryList -> StoryBookListScreen(state = state) + is NorthernLightsStory -> NorthernLightsStory(state = storyState) + ButtonsStory -> ButtonsStory() + FooStory -> FooStory() // stateless + is FooStory -> FooStory(storyState) // stateful (note `is`) +} +``` + +--- + +### 5. Register in `StoryBookListScreen.kt` + +Add one entry to `buildStories()`. **Every title must start with an emoji** that +represents the component category — this makes the list easier to scan at a glance. + +```kotlin +private fun buildStories() = listOf( + StoryItem(title = "🃏 Foo Component", factory = fooStoryFactory), + // existing entries... +) +``` + +Pick an emoji that reflects the component's visual nature or purpose, e.g.: +- Buttons → 🔘 +- Background effects → 🌌 +- Typography → 🔤 +- Icons → 🎨 +- Cards → 🃏 +- Inputs / Text fields → ✏️ +- Navigation → 🧭 +- Loaders / Progress → ⏳ + +--- + +## Design guidelines + +### Layout + +Use a `LazyColumn` as the root for component showcases so the page scrolls +when content is taller than the screen. + +```kotlin +LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxSize().background(TangemTheme.colors2.surface.level1), +) { /* items */ } +``` + +### Showing all variants + +Show every meaningful axis of variation in one place: + +| Axis | How to display | +|---|---| +| **States** (Default, Disabled, Pressed, Loading) | One row per state | +| **Shapes** (Default, Rounded) | One labeled group (`ShapeGroup`) per shape, iterate `TangemButtonShape.entries` | +| **Content** (text+icon vs icon-only) | Two columns per row | +| **Sizes** | Separate `LazyColumn` item per size group if needed | +| **Styles / Effects** (e.g. `TangemMessageEffect`) | Chip toggle — see below | + +> **Prefer vertical stacking over horizontal.** A row should contain at most +> 2–3 components; more than that overflows on narrow screens. Use +> `Modifier.weight(1f)` on columns instead of fixed widths. + +### Toggle for style/effect axes + +When a discrete axis (e.g. a visual effect enum) would produce too many full-width +components on one screen, use a **sticky chip-picker** instead of stacking all values. +Make the page **stateful** and store the selected value in the `StoryBookPage` data class. + +``` +┌─────────────────────────────────┐ ← stickyHeader +│ Magic │ Card │ Warning │ None│ ← chip row (EffectToggle) +└─────────────────────────────────┘ + No icon, no buttons + [ message with selected effect ] + With icon + [ message with selected effect ] + … +``` + +**Pattern:** + +1. Add the selected value + callback to the `StoryBookPage` data class: + ```kotlin + internal data class FooStory( + val selectedVariant: Variant, + val onVariantChange: (Variant) -> Unit, + ) : StoryBookPage + ``` +2. Use a stateful `Build.kt` (see [Step 2](#2-create-pagefoobuildk)). +3. In the story composable, add a `stickyHeader` with a chip row: + ```kotlin + stickyHeader("toggle") { + VariantToggle( + selected = state.selectedVariant, + onSelect = state.onVariantChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + ``` +4. Each `item` below uses `state.selectedVariant` for the component under test. + +See `TangemMessageStory` for a complete example. + +### Section structure (component grids) + +Follow the pattern used in `ButtonsStory`: +- **Section title** — `TangemTheme.typography.subtitle1` +- **Group sub-header** (shape/size/variant name) — `TangemTheme.typography.body2` +- **Column headers** (Text + Icon, Icon only, etc.) — `TangemTheme.typography.caption2` +- **State label** (Default, Disabled…) — `TangemTheme.typography.caption2`, fixed width ~80 dp + +``` +Primary ← subtitle1 + Default ← body2 (shape/group sub-header) + Text + Icon Icon only ← caption2 column headers + Default [■ Continue] [■] ← state row + Disabled [■ Continue] [■] + Pressed [■ Continue] [■] + Loading [ ⟳ ] [⟳] + Rounded ← body2 + ... +``` + +### Colors + +- Page background: `TangemTheme.colors2.surface.level1` +- Sections that need a contrasting background (e.g. PrimaryInverse): + `TangemTheme.colors2.surface.level2` +- Section divider: `HorizontalDivider(color = TangemTheme.colors2.border.neutral.secondary)` + +### Realistic text + +Use representative text strings, not placeholders like "Btn". Pick labels that +match how the component would appear in the product (e.g. `"Continue"`, +`"Send payment"`, `"Confirm"`). + +### DS component imports + +All design system components (`PrimaryTangemButton`, `TangemButtonSize`, etc.) +live in `com.tangem.core.ui.ds.*` and are `public`, so they are directly +importable from the `features/tester` module. + +Use `com.tangem.core.ui.R` for drawable resources (e.g. `R.drawable.ic_tangem_24`). \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 68cab10425..6554b8a54f 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.surveysparrow) /** Core modules */ implementation(projects.core.datasource) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 28d6fe9135..76d4e77f3a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation +import android.widget.Toast import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -17,6 +18,7 @@ import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel import com.tangem.feature.tester.presentation.actions.TesterActionsScreen @@ -36,6 +38,9 @@ import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel +import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen +import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel +import com.tangem.feature.tester.presentation.surveysparrow.SurveySparrowManager import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel import dagger.hilt.android.AndroidEntryPoint @@ -58,6 +63,9 @@ internal class TesterActivity : ComposeActivity() { @Inject lateinit var appRouter: AppRouter + @Inject + lateinit var environmentConfig: EnvironmentConfig + @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary @@ -86,6 +94,8 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TEST_PUSHES, ButtonUM.ACCOUNTS, ButtonUM.ADDRESSES_INFO, + ButtonUM.STORY_BOOK, + ButtonUM.SURVEY_SPARROW, ), onButtonClick = { buttonUM -> val route = when (buttonUM) { @@ -97,6 +107,8 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO + ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK + ButtonUM.SURVEY_SPARROW -> TesterScreen.SURVEY_SPARROW } innerTesterRouter.open(route) @@ -179,6 +191,51 @@ internal class TesterActivity : ComposeActivity() { AddressesInfoScreen(state) } + + composable(route = TesterScreen.STORY_BOOK.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + StoryBookScreen(state) + } + + composable(route = TesterScreen.SURVEY_SPARROW.name) { + LaunchedEffect(Unit) { + val isSuccess = startSurveySparrow() + if (isSuccess) { + innerTesterRouter.back() + } + } + } } } + + private fun startSurveySparrow(): Boolean { + val token = environmentConfig.surveySparrowToken + + if (token.isNullOrEmpty()) { + val toast = Toast.makeText( + this, + "Survey Sparrow is not configured. Token is missing.", + Toast.LENGTH_LONG, + ) + + toast.show() + return false + } + + SurveySparrowManager(domain = DOMAIN, token = token).startSurveyForResult( + activity = this, + requestCode = SURVEY_SPARROW_REQUEST_CODE, + ) + + return true + } + + private companion object { + const val DOMAIN = "tangem.com" + const val SURVEY_SPARROW_REQUEST_CODE = 1001 + } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 2d2f610f44..59523e315d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -26,5 +26,7 @@ data class TesterMenuUM( TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), ADDRESSES_INFO(R.string.addresses_info), + STORY_BOOK(R.string.story_book), + SURVEY_SPARROW(R.string.survey_sparrow), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index ccf4b0acd0..09864f11b0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -15,4 +15,6 @@ internal enum class TesterScreen { TEST_PUSHES, ACCOUNTS, ADDRESSES_INFO, + STORY_BOOK, + SURVEY_SPARROW, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt new file mode 100644 index 0000000000..66cb344ef0 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.core.ui.ds.message.TangemMessageEffect + +internal sealed interface StoryBookPage + +internal data object StoryList : StoryBookPage + +internal data object ButtonsStory : StoryBookPage + +internal data class TangemBadgeStory( + val selectedColor: TangemBadgeColor, + val onColorChange: (TangemBadgeColor) -> Unit, +) : StoryBookPage + +internal data object OpportunitiesBGStory : StoryBookPage + +internal data class TangemCheckboxStory( + val isRoundedChecked: Boolean, + val onRoundedCheckedChange: (Boolean) -> Unit, + val isCircleChecked: Boolean, + val onCircleCheckedChange: (Boolean) -> Unit, +) : StoryBookPage + +internal data object TangemSegmentedPickerStory : StoryBookPage + +internal data class TangemMessageStory( + val selectedEffect: TangemMessageEffect, + val onEffectChange: (TangemMessageEffect) -> Unit, +) : StoryBookPage + +internal data class NorthernLightsStory( + val variant: Variant, + val onVariantChange: (Variant) -> Unit, +) : StoryBookPage { + enum class Variant { + Shader, + Simple, + } +} + +internal data class TangemTokenRowStory( + val isBalanceHidden: Boolean, + val onBalanceHiddenToggle: () -> Unit, +) : StoryBookPage + +internal data class TangemContextMenuStory( + val isExpanded: Boolean, + val onExpandedChange: (Boolean) -> Unit, +) : StoryBookPage + +internal data class TangemHeaderRowStory( + val isBalanceHidden: Boolean, + val onBalanceHiddenToggle: () -> Unit, +) : StoryBookPage \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt new file mode 100644 index 0000000000..39d646a85a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal data class StoryBookUM( + val currentPage: StoryBookPage = StoryList, + val onBackClick: () -> Unit, + val onStoryClick: (StoryPageFactory) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt new file mode 100644 index 0000000000..acea04d55b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal fun interface StoryPageFactory { + fun create(updatePage: ((StoryBookPage) -> StoryBookPage) -> Unit): StoryBookPage +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt new file mode 100644 index 0000000000..e78069bc35 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt @@ -0,0 +1,20 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.background + +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): NorthernLightsStory { + return NorthernLightsStory( + variant = NorthernLightsStory.Variant.Shader, + onVariantChange = { newVariant -> + updateStory { currentState -> + currentState.copy(variant = newVariant) + } + }, + ) +} + +internal val northernLightsStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt new file mode 100644 index 0000000000..8f0fd1f280 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -0,0 +1,92 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.background + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory + +@Composable +internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize()) { + NorthernLightsBackground( + modifier = Modifier.fillMaxSize(), + containerColor = TangemTheme.colors2.surface.level1, + forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple, + ) + + NorthernLightsVariantToggle( + selected = state.variant, + onSelect = state.onVariantChange, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(bottom = 24.dp) + .padding(horizontal = 24.dp), + ) + } +} + +@Composable +private fun NorthernLightsVariantToggle( + selected: NorthernLightsStory.Variant, + onSelect: (NorthernLightsStory.Variant) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(Color.Black.copy(alpha = 0.35f)) + .border(width = 1.dp, color = Color.White.copy(alpha = 0.15f), shape = shape) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + NorthernLightsStory.Variant.entries.forEach { variant -> + VariantChip( + label = variant.label, + selected = variant == selected, + onClick = { onSelect(variant) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun VariantChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background(if (selected) Color.White.copy(alpha = 0.2f) else Color.Transparent) + .clickable(onClick = onClick) + .padding(vertical = 10.dp, horizontal = 16.dp), + ) { + Text( + text = label, + color = Color.White, + fontSize = 14.sp, + ) + } +} + +private val NorthernLightsStory.Variant.label: String + get() = when (this) { + NorthernLightsStory.Variant.Shader -> "Shader" + NorthernLightsStory.Variant.Simple -> "Simple" + } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt new file mode 100644 index 0000000000..28eba8975c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt @@ -0,0 +1,19 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.badge + +import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemBadgeStory { + return TangemBadgeStory( + selectedColor = TangemBadgeColor.Blue, + onColorChange = { color -> + updateStory { it.copy(selectedColor = color) } + }, + ) +} + +internal val tangemBadgeStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt new file mode 100644 index 0000000000..96f315a25c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt @@ -0,0 +1,224 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.badge + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory + +private const val STATE_LABEL_WIDTH = 80 + +@Composable +internal fun TangemBadgeStory(state: TangemBadgeStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("color_toggle") { + ColorToggle( + selected = state.selectedColor, + onSelect = state.onColorChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + TangemBadgeSize.entries.forEach { size -> + item(size.name) { + BadgeSizeSection(size = size, color = state.selectedColor) + } + } + } +} + +@Composable +private fun ColorToggle( + selected: TangemBadgeColor, + onSelect: (TangemBadgeColor) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border(width = 1.dp, color = TangemTheme.colors2.border.neutral.secondary, shape = shape) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemBadgeColor.entries.forEach { color -> + ColorChip( + label = color.name, + selected = color == selected, + onClick = { onSelect(color) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun ColorChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background(if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +private fun BadgeSizeSection(size: TangemBadgeSize, color: TangemBadgeColor) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = size.name, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + TangemBadgeShape.entries.forEach { shape -> + BadgeShapeGroup(size = size, shape = shape, color = color) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun BadgeShapeGroup(size: TangemBadgeSize, shape: TangemBadgeShape, color: TangemBadgeColor) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = shape.name, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + ColumnHeaderRow() + TangemBadgeType.entries.forEach { type -> + BadgeTypeRow(size = size, shape = shape, color = color, type = type) + } + } +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Text + Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Text", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun BadgeTypeRow( + size: TangemBadgeSize, + shape: TangemBadgeShape, + color: TangemBadgeColor, + type: TangemBadgeType, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = type.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + text = stringReference("New"), + tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_information_24), + size = size, + shape = shape, + color = color, + type = type, + iconPosition = TangemBadgeIconPosition.Start, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + text = stringReference("New"), + size = size, + shape = shape, + color = color, + type = type, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24), + size = size, + shape = shape, + color = color, + type = type, + iconPosition = TangemBadgeIconPosition.Start, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt new file mode 100644 index 0000000000..00af9f20b0 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt @@ -0,0 +1,7 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.buttons + +import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val buttonsStoryFactory: StoryPageFactory = StoryPageFactory { ButtonsStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt new file mode 100644 index 0000000000..9d9a6a9c09 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt @@ -0,0 +1,202 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme + +private const val STATE_LABEL_WIDTH = 80 + +@Suppress("LongMethod") +@Composable +internal fun ButtonsStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("primary") { + ButtonSection(title = "Primary") { state, text, shape -> + PrimaryTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("secondary") { + ButtonSection(title = "Secondary") { state, text, shape -> + SecondaryTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("primary_inverse") { + ButtonSection( + title = "PrimaryInverse", + background = TangemTheme.colors2.surface.level2, + ) { state, text, shape -> + PrimaryInverseTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("outline") { + ButtonSection(title = "Outline") { state, text, shape -> + OutlineTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("accent") { + ButtonSection(title = "Accent") { state, text, shape -> + AccentTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("ghost") { + ButtonSection(title = "Ghost") { state, text, shape -> + GhostTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + } +} + +@Composable +private fun ButtonSection( + title: String, + background: Color = TangemTheme.colors2.surface.level1, + shapes: List = TangemButtonShape.entries, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + shapes.forEach { shape -> + ShapeGroup(shape = shape, button = button) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ShapeGroup( + shape: TangemButtonShape, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = shape.name, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + ColumnHeaderRow() + TangemButtonState.entries.forEach { state -> + StateRow(state = state, shape = shape, button = button) + } + } +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Text + Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Icon only", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun StateRow( + state: TangemButtonState, + shape: TangemButtonShape, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box(modifier = Modifier.weight(1f)) { + button(state, true, shape) + } + Box(modifier = Modifier.weight(1f)) { + button(state, false, shape) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt new file mode 100644 index 0000000000..cc54b661b4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt @@ -0,0 +1,22 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.checkbox + +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemCheckboxStory { + return TangemCheckboxStory( + isRoundedChecked = false, + onRoundedCheckedChange = { checked -> + updateStory { it.copy(isRoundedChecked = checked) } + }, + isCircleChecked = false, + onCircleCheckedChange = { checked -> + updateStory { it.copy(isCircleChecked = checked) } + }, + ) +} + +internal val tangemCheckboxStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt new file mode 100644 index 0000000000..fbba0e7b64 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt @@ -0,0 +1,137 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.checkbox + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +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.ds.checkbox.TangemCheckbox +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory + +private const val STATE_LABEL_WIDTH = 80 + +@Composable +internal fun TangemCheckboxStory(state: TangemCheckboxStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("grid") { + CheckboxGrid(state = state) + } + } +} + +@Composable +private fun CheckboxGrid(state: TangemCheckboxStory) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + ColumnHeaderRow() + CheckboxRow( + label = "Rounded", + isChecked = state.isRoundedChecked, + onCheckedChange = state.onRoundedCheckedChange, + isRounded = true, + ) + CheckboxRow( + label = "Circle", + isChecked = state.isCircleChecked, + onCheckedChange = state.onCircleCheckedChange, + isRounded = false, + ) + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Enabled", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Disabled", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Disabled (on)", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun CheckboxRow(label: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, isRounded: Boolean) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = isChecked, + isRounded = isRounded, + isEnabled = true, + onCheckedChange = onCheckedChange, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = false, + isRounded = isRounded, + isEnabled = false, + onCheckedChange = {}, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = true, + isRounded = isRounded, + isEnabled = false, + onCheckedChange = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt new file mode 100644 index 0000000000..87ed7bf20e --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.tester.presentation.storybook.page.contextmenu + +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemContextMenuStory { + return TangemContextMenuStory( + isExpanded = false, + onExpandedChange = { expanded -> + updateStory { it.copy(isExpanded = expanded) } + }, + ) +} + +internal val tangemContextMenuStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt new file mode 100644 index 0000000000..8bac2d248c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt @@ -0,0 +1,80 @@ +package com.tangem.feature.tester.presentation.storybook.page.contextmenu + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.ds.contextmenu.TangemContextMenu +import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import dev.chrisbanes.haze.rememberHazeState +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory as TangemContextMenuStoryState + +@Composable +internal fun TangemContextMenuStory(state: TangemContextMenuStoryState, modifier: Modifier = Modifier) { + val hazeState = rememberHazeState() + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1) + .hazeSourceTangem(state = hazeState, zIndex = -1f), + ) { + item("context_menu") { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = "TangemContextMenu", + style = TangemTheme.typography2.headingSemibold17, + color = TangemTheme.colors2.text.neutral.primary, + ) + + Box { + PrimaryButton( + text = "Show Context Menu", + onClick = { state.onExpandedChange(true) }, + ) + + TangemContextMenu( + expanded = state.isExpanded, + onDismissRequest = { state.onExpandedChange(false) }, + offset = DpOffset(0.dp, 4.dp), + modifier = Modifier.hazeEffectTangem(hazeState), + ) { + TangemContextMenuCheckboxItem( + title = TextReference.Str("Sort by balance"), + isChecked = true, + onClick = {}, + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors2.border.neutral.quaternary, + ) + TangemContextMenuCheckboxItem( + title = TextReference.Str("Group tokens"), + isChecked = false, + onClick = {}, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/Build.kt new file mode 100644 index 0000000000..200ac7a796 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/Build.kt @@ -0,0 +1,17 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.headerrow + +import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemHeaderRowStory { + return TangemHeaderRowStory( + isBalanceHidden = false, + onBalanceHiddenToggle = { updateStory { it.copy(isBalanceHidden = !it.isBalanceHidden) } }, + ) +} + +internal val tangemHeaderRowStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt new file mode 100644 index 0000000000..95a5c349d7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt @@ -0,0 +1,111 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.headerrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Switch +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.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory + +@Composable +internal fun TangemHeaderRowStory(state: TangemHeaderRowStory, modifier: Modifier = Modifier) { + val rows = remember { buildSampleRows() } + + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("balance_toggle") { + BalanceToggle( + isHidden = state.isBalanceHidden, + onToggle = state.onBalanceHiddenToggle, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + items(rows, key = { it.id }) { um -> + TangemHeaderRow( + headerRowUM = um, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier.background(TangemTheme.colors2.surface.level3), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(start = 16.dp), + ) + } + } +} + +private fun buildSampleRows(): List = listOf( + TangemHeaderRowUM( + id = "icon_subtitle_no_tail", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Empty, + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "icon_subtitle_collapse", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Icon(R.drawable.ic_arrow_collapse_24), + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "icon_subtitle_group_drop", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Icon(R.drawable.ic_group_drop_24), + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "title_only", + title = stringReference("Account"), + ), +) + +@Composable +private fun BalanceToggle(isHidden: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier, + ) { + Text( + text = "isBalanceHidden", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Switch(checked = isHidden, onCheckedChange = { onToggle() }) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt new file mode 100644 index 0000000000..8f357d4941 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt @@ -0,0 +1,19 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.message + +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemMessageStory { + return TangemMessageStory( + selectedEffect = TangemMessageEffect.None, + onEffectChange = { newEffect -> + updateStory { it.copy(selectedEffect = newEffect) } + }, + ) +} + +internal val tangemMessageStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt new file mode 100644 index 0000000000..f43e8ec903 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt @@ -0,0 +1,233 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.message + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.* +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TangemMessageStory(state: TangemMessageStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .statusBarsPadding() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("effect_toggle") { + EffectToggle( + selected = state.selectedEffect, + onSelect = state.onEffectChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + item("plain") { + VariantSection(label = "No icon, no buttons") { + TangemMessage( + messageUM = TangemMessageUM( + id = "plain", + title = stringReference("Update available"), + subtitle = stringReference("A new firmware version is ready to install on your card."), + messageEffect = state.selectedEffect, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("icon") { + VariantSection(label = "With icon") { + TangemMessage( + messageUM = TangemMessageUM( + id = "icon", + title = stringReference("Wallet backup missing"), + subtitle = stringReference("To protect your assets, complete the backup process."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("centered") { + VariantSection(label = "Centered") { + TangemMessage( + messageUM = TangemMessageUM( + id = "centered", + title = stringReference("Scan your card"), + subtitle = stringReference("Hold the card to the back of your phone."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + isCentered = true, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("1btn") { + VariantSection(label = "With 1 button") { + TangemMessage( + messageUM = TangemMessageUM( + id = "1btn", + title = stringReference("Generate addresses"), + subtitle = stringReference("Generate addresses for 2 new networks using your card."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = stringReference("Generate"), + type = TangemButtonType.Primary, + iconRes = R.drawable.ic_tangem_24, + onClick = {}, + ), + ), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("2btn") { + VariantSection(label = "With 2 buttons") { + TangemMessage( + messageUM = TangemMessageUM( + id = "2btn", + title = stringReference("Rate the app"), + subtitle = stringReference("How do you like Tangem so far?"), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = stringReference("Love it!"), + type = TangemButtonType.PrimaryInverse, + onClick = {}, + ), + TangemMessageButtonUM( + text = stringReference("Can be better"), + type = TangemButtonType.Primary, + onClick = {}, + ), + ), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("close") { + VariantSection(label = "With close button") { + TangemMessage( + messageUM = TangemMessageUM( + id = "close", + title = stringReference("Note top up"), + subtitle = stringReference("To activate the card, top it up with at least 1 XLM."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + onCloseClick = {}, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun EffectToggle( + selected: TangemMessageEffect, + onSelect: (TangemMessageEffect) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.secondary, + shape = shape, + ) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemMessageEffect.entries.forEach { effect -> + EffectChip( + label = effect.name, + selected = effect == selected, + onClick = { onSelect(effect) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun EffectChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background( + if (selected) { + TangemTheme.colors2.surface.level3 + } else { + TangemTheme.colors2.surface.level2 + }, + ) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + } +} + +@Composable +private fun VariantSection(label: String, content: @Composable ColumnScope.() -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + content() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt new file mode 100644 index 0000000000..12f5d224e7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt @@ -0,0 +1,7 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.opportunities + +import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val opportunitiesBGStoryFactory: StoryPageFactory = StoryPageFactory { OpportunitiesBGStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt new file mode 100644 index 0000000000..ca09305652 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt @@ -0,0 +1,122 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.opportunities + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +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.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.res.TangemTheme + +private data class IconVariant( + val label: String, + val icon: TangemIconUM, +) + +private val variants = listOf( + IconVariant( + label = "Bitcoin", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Solana", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Avalanche", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_avalanche_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "BNB Smart Chain", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_bsc_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Cardano", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_cardano_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), +) + +@Composable +internal fun OpportunitiesBGStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + variants.forEach { variant -> + item(variant.label) { + OpportunitiesBG( + icon = variant.icon, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp), + ) { + TangemIcon( + tangemIconUM = variant.icon, + modifier = Modifier.size(40.dp), + ) + Text( + text = variant.label, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt new file mode 100644 index 0000000000..e8dd4f694f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt @@ -0,0 +1,7 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tabs + +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory + +internal val tangemSegmentedPickerStoryFactory: StoryPageFactory = StoryPageFactory { TangemSegmentedPickerStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt new file mode 100644 index 0000000000..df03dcb6d4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt @@ -0,0 +1,171 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tabs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.tabs.TangemSegmentUM +import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +private val items2 = persistentListOf( + TangemSegmentUM(id = "all", title = stringReference("All")), + TangemSegmentUM(id = "tokens", title = stringReference("Tokens")), +) + +private val items3 = persistentListOf( + TangemSegmentUM(id = "1d", title = stringReference("1D")), + TangemSegmentUM(id = "1w", title = stringReference("1W")), + TangemSegmentUM(id = "1m", title = stringReference("1M")), +) + +private val items4 = persistentListOf( + TangemSegmentUM(id = "1d", title = stringReference("1D")), + TangemSegmentUM(id = "1w", title = stringReference("1W")), + TangemSegmentUM(id = "1m", title = stringReference("1M")), + TangemSegmentUM(id = "1y", title = stringReference("1Y")), +) + +private val items5 = persistentListOf( + TangemSegmentUM(id = "send", title = stringReference("Send")), + TangemSegmentUM(id = "receive", title = stringReference("Receive")), + TangemSegmentUM(id = "swap", title = stringReference("Swap")), + TangemSegmentUM(id = "buy", title = stringReference("Buy")), + TangemSegmentUM(id = "sell", title = stringReference("Sell")), +) + +private data class PickerConfig( + val label: String, + val hasSeparator: Boolean, + val isFixed: Boolean, +) + +private val configs = listOf( + PickerConfig("Default", hasSeparator = false, isFixed = false), + PickerConfig("Separator", hasSeparator = true, isFixed = false), + PickerConfig("Fixed", hasSeparator = false, isFixed = true), + PickerConfig("Fixed + Separator", hasSeparator = true, isFixed = true), +) + +@Composable +internal fun TangemSegmentedPickerStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("default_surface") { + PickerSection( + title = "Default surface", + isAltSurface = false, + background = TangemTheme.colors2.surface.level1, + ) + } + item("alt_surface") { + PickerSection( + title = "Alt surface", + isAltSurface = true, + background = TangemTheme.colors2.surface.level2, + ) + } + item("segment_count") { + SegmentCountSection() + } + } +} + +@Composable +private fun PickerSection(title: String, isAltSurface: Boolean, background: Color) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + configs.forEach { config -> + PickerRow(config = config, isAltSurface = isAltSurface) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun PickerRow(config: PickerConfig, isAltSurface: Boolean) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = config.label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemSegmentedPicker( + items = items4, + hasSeparator = config.hasSeparator, + isFixed = config.isFixed, + isAltSurface = isAltSurface, + onClick = {}, + modifier = if (config.isFixed) Modifier.fillMaxWidth() else Modifier, + ) + } +} + +@Composable +private fun SegmentCountSection() { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = "Segment count", + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SegmentCountRow(label = "2 segments", items = items2) + SegmentCountRow(label = "3 segments", items = items3) + SegmentCountRow(label = "4 segments", items = items4) + SegmentCountRow(label = "5 segments", items = items5) + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun SegmentCountRow(label: String, items: ImmutableList) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemSegmentedPicker( + items = items, + isFixed = true, + onClick = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt new file mode 100644 index 0000000000..988f75af8a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt @@ -0,0 +1,16 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tokenrow + +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemTokenRowStory { + return TangemTokenRowStory( + isBalanceHidden = false, + onBalanceHiddenToggle = { updateStory { it.copy(isBalanceHidden = !it.isBalanceHidden) } }, + ) +} + +internal val tangemTokenRowStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt new file mode 100644 index 0000000000..d87c97f642 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt @@ -0,0 +1,73 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.tokenrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Switch +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.unit.dp +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRow_PreviewProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory + +@Composable +internal fun TangemTokenRowStory(state: TangemTokenRowStory, modifier: Modifier = Modifier) { + val rows = remember { TangemTokenRow_PreviewProvider().values.toList() } + + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("balance_toggle") { + BalanceToggle( + isHidden = state.isBalanceHidden, + onToggle = state.onBalanceHiddenToggle, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + items(rows, key = { it.id }) { um -> + TangemTokenRow( + tokenRowUM = um, + isBalanceHidden = state.isBalanceHidden, + reorderableState = null, + modifier = Modifier.background(TangemTheme.colors2.surface.level1), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(start = 16.dp), + ) + } + } +} + +@Composable +private fun BalanceToggle(isHidden: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier, + ) { + Text( + text = "isBalanceHidden", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Switch(checked = isHidden, onCheckedChange = { onToggle() }) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt new file mode 100644 index 0000000000..6fc4111d73 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.badge.tangemBadgeStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.buttons.buttonsStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.checkbox.tangemCheckboxStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.contextmenu.tangemContextMenuStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.headerrow.tangemHeaderRowStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.tabs.tangemSegmentedPickerStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.tokenrow.tangemTokenRowStoryFactory + +private data class StoryItem(val title: String, val factory: StoryPageFactory) + +private fun buildStories() = listOf( + StoryItem(title = "🔘 Buttons", factory = buttonsStoryFactory), + StoryItem(title = "🏷️ Badge", factory = tangemBadgeStoryFactory), + StoryItem(title = "✨ Opportunities BG", factory = opportunitiesBGStoryFactory), + StoryItem(title = "🌌 Northern Lights Background", factory = northernLightsStoryFactory), + StoryItem(title = "💬 Message", factory = tangemMessageStoryFactory), + StoryItem(title = "🗂️ Segmented Picker", factory = tangemSegmentedPickerStoryFactory), + StoryItem(title = "☑️ Checkbox", factory = tangemCheckboxStoryFactory), + StoryItem(title = "🪙 Token Row", factory = tangemTokenRowStoryFactory), + StoryItem(title = "📑 Header Row", factory = tangemHeaderRowStoryFactory), + StoryItem(title = "📋 Context Menu", factory = tangemContextMenuStoryFactory), +) + +@Composable +internal fun StoryBookListScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + val stories = remember { buildStories() } + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = "Storybook", + containerColor = TangemTheme.colors.background.primary, + ) + } + + items(items = stories, key = { it.title }) { item -> + PrimaryButton( + text = item.title, + onClick = { state.onStoryClick(item.factory) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt new file mode 100644 index 0000000000..f3e58f0000 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.page.badge.TangemBadgeStory +import com.tangem.feature.tester.presentation.storybook.page.buttons.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.page.checkbox.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory +import com.tangem.feature.tester.presentation.storybook.page.contextmenu.TangemContextMenuStory + +@Composable +internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + AnimatedContent( + targetState = state.currentPage, + contentKey = { it::class }, + modifier = modifier, + ) { storyState -> + when (storyState) { + StoryList -> StoryBookListScreen(state = state) + is NorthernLightsStory -> NorthernLightsStory(state = storyState) + ButtonsStory -> ButtonsStory() + is TangemBadgeStory -> TangemBadgeStory(state = storyState) + OpportunitiesBGStory -> OpportunitiesBGStory() + is TangemMessageStory -> TangemMessageStory(state = storyState) + is TangemCheckboxStory -> TangemCheckboxStory(state = storyState) + TangemSegmentedPickerStory -> TangemSegmentedPickerStory() + is TangemTokenRowStory -> TangemTokenRowStory(state = storyState) + is TangemHeaderRowStory -> TangemHeaderRowStory(state = storyState) + is TangemContextMenuStory -> TangemContextMenuStory(state = storyState) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt new file mode 100644 index 0000000000..851570c925 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookPage +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal interface StateUpdater { + fun updateStory(update: (T) -> T) +} + +internal inline fun storyPageFactory( + crossinline build: StateUpdater.() -> T, +): StoryPageFactory = StoryPageFactory { updatePage -> + val updater = object : StateUpdater { + override fun updateStory(update: (T) -> T) { + updatePage { current -> + if (current is T) update(current) else current + } + } + } + updater.build() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt new file mode 100644 index 0000000000..e08ab8c0d7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +internal class StoryBookViewModel @Inject constructor() : ViewModel() { + + private var router: InnerTesterRouter? = null + + private val _uiState = MutableStateFlow( + StoryBookUM( + onBackClick = ::onBackClick, + onStoryClick = ::onStoryClick, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun onBackClick() { + if (_uiState.value.currentPage !is StoryList) { + _uiState.update { it.copy(currentPage = StoryList) } + } else { + router?.back() + } + } + + private fun onStoryClick(factory: StoryPageFactory) { + _uiState.update { state -> + state.copy( + currentPage = factory.create { update -> + _uiState.update { s -> s.copy(currentPage = update(s.currentPage)) } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt new file mode 100644 index 0000000000..5a91649f19 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.tester.presentation.surveysparrow + +import android.app.Activity +import com.surveysparrow.ss_android_sdk.SsSurvey +import com.surveysparrow.ss_android_sdk.SurveySparrow +import timber.log.Timber + +/** + * Manager for Survey Sparrow SDK. + * + * @param domain Survey Sparrow domain (e.g., "yourcompany") + * @param token Survey Sparrow SDK token + */ +class SurveySparrowManager( + private val domain: String, + private val token: String, +) { + + /** + * Create a SurveySparrow instance to start a survey. + * + * @param activity The activity context + * @param customVariables Optional custom variables to pass to the survey + * @return SurveySparrow instance ready to start + */ + fun createSurvey(activity: Activity, customVariables: Map? = null): SurveySparrow? { + return try { + val survey = SsSurvey(domain, token).apply { + customVariables?.forEach { (key, value) -> + addCustomParam(key, value) + } + } + + SurveySparrow(activity, survey) + } catch (e: Exception) { + Timber.e(e, "Failed to create SurveySparrow survey") + null + } + } + + /** + * Start a survey for result. + * + * @param activity The activity context + * @param requestCode The request code for onActivityResult + * @param customVariables Optional custom variables to pass to the survey + */ + fun startSurveyForResult(activity: Activity, requestCode: Int, customVariables: Map? = null) { + val surveySparrow = createSurvey(activity, customVariables) + if (surveySparrow != null) { + surveySparrow.startSurveyForResult(requestCode) + Timber.d("SurveySparrow survey started with requestCode: $requestCode") + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9aadd20368..f6d9e5cf50 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -22,4 +22,6 @@ News details News details (Bottom Sheet) Addresses info + Story book + Survey Sparrow diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 10cb869ad1..3b4adc992f 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -69,10 +69,10 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.markets.models) implementation(projects.domain.models) implementation(projects.domain.notifications.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) @@ -108,6 +108,7 @@ dependencies { implementation(projects.features.sendV2.api) implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) + implementation(projects.features.tangempay.details.api) implementation(deps.decompose.ext.compose) diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index 71ab124fb2..23e783a751 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -13,17 +13,8 @@ BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, statusModel) BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, transaction.status) BooleanPropertyNaming:TokenDetailsTopAppBar.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) } - MultilineLambdaItParameter:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true val isCorrectDerivation = isDefaultDerivation || isCustomDerivation isNetwork && isCurrency && isCorrectDerivation } MultilineLambdaItParameter:ExpressStatusFactory.kt$ExpressStatusFactory${ when (it) { is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden else -> false } } MultilineLambdaItParameter:OnrampStatusFactory.kt$OnrampStatusFactory${ Timber.e("Couldn't update onramp status. $it") onrampTx } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ Timber.e(it.cause?.localizedMessage.orEmpty()) "" } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send( TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), ) shareManager.shareText(text = it) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent( exception = it, params = mapOf( "blockchainId" to cryptoCurrency.network.id.rawId.value, "networkId" to cryptoCurrency.network.backendId, ), ), ) Timber.e( /* t = */ it, /* message = */ "Unable to get wallet manager for user wallet %s and network %s", /* ...args = */ userWalletId, cryptoCurrency.network, ) false } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) Timber.e(it) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ val updatedState = stateFactory.getStateWithNotifications(it) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } MultilineLambdaItParameter:TokenDetailsScreen.kt${ Notification( modifier = itemModifier.animateItem(), config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) } MultilineLambdaItParameter:TokenDetailsTopAppBar.kt${ TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TokenStakingBlock.kt${ when (it) { is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock() is StakingBlockUM.Loading -> StakingLoading() is StakingBlockUM.Staked -> StakingBalanceBlock( state = it, isBalanceHidden = isBalanceHidden, ) is StakingBlockUM.StakeAvailable -> StakingAvailableContent( state = it, ) } } @@ -31,21 +22,15 @@ NamedArguments:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$createStateInfo( transaction, toCryptoCurrency, fromCryptoCurrency, toFiatAmount, fromFiatAmount, ) NestedScopeFunctions:TokenDetailsBalanceSelectStateConverter.kt$TokenDetailsBalanceSelectStateConverter$let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } NullableBooleanCheck:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$transaction.status?.hasLongTime ?: false - NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$networkId - NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingCryptoAmount NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingEntryInfo PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$network PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$token - PropertyUsedBeforeDeclaration:TokenDetailsModel.kt$TokenDetailsModel$uiState SuspendFunSwallowedCancellation:ExchangeStatusFactory.kt$ExchangeStatusFactory$runCatching - SuspendFunSwallowedCancellation:TokenDetailsModel.kt$TokenDetailsModel$runCatching - UnnecessaryLet:TokenDetailsModel.kt$TokenDetailsModel$let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) } UnnecessaryLet:TokenDetailsSkeletonStateConverter.kt$TokenDetailsSkeletonStateConverter$let(::add) UnnecessaryLet:TokenDetailsStateFactory.kt$TokenDetailsStateFactory$let(::add) UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent$mapOf() UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent.Notice$mapOf() UseOrEmpty:ExchangeStatusFactory.kt$ExchangeStatusFactory$savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.toSet() ?.getQuotesOrEmpty() ?: emptySet() - VarCouldBeVal:TokenDetailsModel.kt$TokenDetailsModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 345620dd50..64c0be0735 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -9,7 +9,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -18,11 +17,15 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.notifications.models.NotificationType -import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger import dagger.assisted.Assisted @@ -39,7 +42,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( @Assisted private val isFromOnNewIntent: Boolean, private val appRouter: AppRouter, private val selectWalletUseCase: SelectWalletUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger, @@ -47,7 +49,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val getUserWalletUseCase: GetUserWalletUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, - private val accountsFeatureToggles: AccountsFeatureToggles, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) : TokenDetailsDeepLinkHandler { @@ -128,7 +130,10 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( id = cryptoCurrency.id, ) !isMultiCurrency -> walletBalanceFetcher( - params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId), + params = WalletBalanceFetcher.Params( + userWalletId = userWallet.walletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), ) } } @@ -137,12 +142,12 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( if (userWallet.isMultiCurrency) { val derivationPath = queryParams[DERIVATION_PATH_KEY] - getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { - val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) - val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true + getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { currency -> + val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true) + val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true - val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card - val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true + val isDefaultDerivation = currency.network.derivationPath is Network.DerivationPath.Card + val isCustomDerivation = derivationPath?.equals(currency.network.derivationPath.value) == true val isCorrectDerivation = isDefaultDerivation || isCustomDerivation isNetwork && isCurrency && isCorrectDerivation } @@ -151,14 +156,9 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } private suspend fun getCryptoCurrencies(userWalletId: UserWalletId): List? { - return if (accountsFeatureToggles.isFeatureEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ) - ?.toList() - } else { - getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrNull() - } + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), + )?.toList() } @AssistedFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index a221f254ec..fa2e9d6908 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -176,7 +176,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isYieldSupplyFeatureEnabled = false, ) val tokenDetailsState_2 = TokenDetailsState( @@ -202,7 +201,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isYieldSupplyFeatureEnabled = true, ) val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock) 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 5e7a0d7096..bd24cb6cc5 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 @@ -2,24 +2,20 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model 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.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.ExpressStateFactory @@ -43,10 +39,8 @@ internal class ExpressTransactionsModel @Inject constructor( paramsContainer: ParamsContainer, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val router: InnerTokenDetailsRouter, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, ) : Model(), ExpressTransactionsClickIntents { @@ -163,22 +157,10 @@ internal class ExpressTransactionsModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) - .onEach { account = it.account } - .map { it.status.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .onEach { account = it.account } .distinctUntilChanged() - .onEach { maybeCurrencyStatus -> - maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status } - } + .onEach { cryptoCurrencyStatus = it.status } .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(marketPriceJobHolder) 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 f06bee0ec8..0dc6645e21 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 @@ -18,11 +18,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -31,7 +33,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.SnackbarMessage -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.GetSelectedAppCurrencyUseCase @@ -47,15 +48,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoTokenUseCase import com.tangem.domain.promo.models.PromoId -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent @@ -91,7 +91,6 @@ import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -105,17 +104,16 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration") @Stable @ModelScoped internal class TokenDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val isCryptoCurrencyCoinCouldHideUseCase: IsCryptoCurrencyCoinCouldHideUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -130,7 +128,8 @@ internal class TokenDetailsModel @Inject constructor( private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase, private val openTrustlineUseCase: OpenTrustlineUseCase, private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, @@ -144,11 +143,9 @@ internal class TokenDetailsModel @Inject constructor( private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, @@ -177,7 +174,7 @@ internal class TokenDetailsModel @Inject constructor( private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var account: Account.CryptoPortfolio? = null private var isBalanceLoadedEventSent = false - private var expressTxStatusTaskScheduler = SingleTaskScheduler>() + private val expressTxStatusTaskScheduler = SingleTaskScheduler>() /** Transaction id to check for status */ private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) @@ -193,7 +190,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) @@ -266,21 +262,13 @@ internal class TokenDetailsModel @Inject constructor( private fun initButtons() { // we need also init buttons before start all loading to avoid buttons blocking modelScope.launch { - val currentCryptoCurrencyStatus = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrency, - ) - .onSome { account = it.account } - .getOrNull() - ?.status - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( - userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ).getOrNull() - } + val currentCryptoCurrencyStatus = getAccountCryptoCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + currency = cryptoCurrency, + ) + .onSome { account = it.account } + .getOrNull() + ?.status currentCryptoCurrencyStatus?.let { status -> cryptoCurrencyStatus = status @@ -297,9 +285,9 @@ internal class TokenDetailsModel @Inject constructor( private fun handleBalanceHiding() { getBalanceHidingSettingsUseCase() - .onEach { + .onEach { settings -> internalUiState.value = stateFactory.getStateWithUpdatedHidden( - isBalanceHidden = it.isBalanceHidden, + isBalanceHidden = settings.isBalanceHidden, ) } .launchIn(modelScope) @@ -312,9 +300,9 @@ internal class TokenDetailsModel @Inject constructor( ) .conflate() .distinctUntilChanged() - .onEach { - sendButtonsEvents(it.states) - internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) + .onEach { state -> + sendButtonsEvents(state.states) + internalUiState.value = stateFactory.getManageButtonsState(actions = state.states) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -346,8 +334,8 @@ internal class TokenDetailsModel @Inject constructor( userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() - .onEach { - val updatedState = stateFactory.getStateWithNotifications(it) + .onEach { warnings -> + val updatedState = stateFactory.getStateWithNotifications(warnings) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } @@ -357,18 +345,9 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) - .onEach { account = it.account } - .map { it.status.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .onEach { account = it.account } + .map { it.status.right() } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) @@ -403,7 +382,7 @@ internal class TokenDetailsModel @Inject constructor( isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { - runCatching { + runSuspendCatching { expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) } }, @@ -423,9 +402,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnYieldSupplyBalanceIfActive(status: CryptoCurrencyStatus) { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + if (status.value.yieldSupplyStatus?.isActive == true) { if (yieldSupplyBalanceJobHolder.isActive && status.value.sources.networkSource != StatusSource.ACTUAL) { return } @@ -511,10 +488,10 @@ internal class TokenDetailsModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrency.network, ) - .mapLeft { + .mapLeft { throwable -> analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent( - exception = it, + exception = throwable, params = mapOf( "blockchainId" to cryptoCurrency.network.id.rawId.value, "networkId" to cryptoCurrency.network.backendId, @@ -523,7 +500,7 @@ internal class TokenDetailsModel @Inject constructor( ) Timber.e( - /* t = */ it, + /* t = */ throwable, /* message = */ "Unable to get wallet manager for user wallet %s and network %s", /* ...args = */ userWalletId, cryptoCurrency.network, @@ -677,8 +654,8 @@ internal class TokenDetailsModel @Inject constructor( userWalletId, cryptoCurrency.network, ).fold( - ifLeft = { - Timber.e(it.cause?.localizedMessage.orEmpty()) + ifLeft = { throwable -> + Timber.e(throwable.cause?.localizedMessage.orEmpty()) "" }, ifRight = { it }, @@ -709,12 +686,13 @@ internal class TokenDetailsModel @Inject constructor( showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = status, - appCurrencyCode = selectedAppCurrencyFlow.value.code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventsHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } @@ -760,29 +738,34 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol)) modelScope.launch { - val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency) - internalUiState.value = if (hasLinkedTokens) { - stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) - } else { + val canHide = when (cryptoCurrency) { + is CryptoCurrency.Coin -> { + isCryptoCurrencyCoinCouldHideUseCase( + userWalletId = userWalletId, + cryptoCurrencyCoin = cryptoCurrency, + ) + } + is CryptoCurrency.Token -> true + } + + internalUiState.value = if (canHide) { stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + } else { + stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } } } override fun onHideConfirmed() { modelScope.launch { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = account?.accountId + val accountId = account?.accountId - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") - return@launch - } - - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) - } else { - removeCurrencyUseCase(userWalletId, cryptoCurrency) + if (accountId == null) { + Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") + return@launch } + + manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) .onLeft { Timber.e(it) } .onRight { router.popBackStack() } } @@ -978,9 +961,9 @@ internal class TokenDetailsModel @Inject constructor( } } } - message?.let { - internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) - Timber.e(it) + if (message != null) { + internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message)) + Timber.e(message) } }, ifRight = { @@ -1021,7 +1004,10 @@ internal class TokenDetailsModel @Inject constructor( is SendTransactionError.UnknownError -> error.ex?.localizedMessage }?.let { stringReference(it) } } - message?.let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) } + + if (message != null) { + internalUiState.value = stateFactory.getStateWithErrorDialog(message) + } }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, ) @@ -1235,13 +1221,11 @@ internal class TokenDetailsModel @Inject constructor( } private suspend fun needShowYieldSupplyWarning(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun isActiveYieldSupply(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true + return cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true } override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index ed8cf22b9f..8fc3962fe6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -23,5 +23,4 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isYieldSupplyFeatureEnabled: Boolean, ) \ No newline at end of file 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 0b10013609..16a861d7c9 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 @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsYieldSupplyState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter @@ -31,7 +30,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter, TokenDetailsState> { override fun convert(value: Either): TokenDetailsState { @@ -113,10 +111,7 @@ internal class TokenDetailsLoadedBalanceConverter( selectedBalanceType = currentState.selectedBalanceType, isBalanceSelectorEnabled = isBalanceSelectorEnabled, isBalanceFlickering = status.value.isFlickering(), - yieldSupplyState = - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + yieldSupplyState = if (status.value.yieldSupplyStatus?.isActive == true) { TokenDetailsYieldSupplyState.Active(clickIntents::onYieldInfoClick) } else { TokenDetailsYieldSupplyState.Empty diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 4bf97d2e4d..5580bcc639 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList @@ -31,7 +30,6 @@ internal class TokenDetailsSkeletonStateConverter( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -72,7 +70,6 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 323b323e72..aa14a1aa20 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -4,7 +4,6 @@ import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -33,8 +32,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList @@ -48,7 +47,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) { private val skeletonStateConverter by lazy { @@ -57,7 +55,6 @@ internal class TokenDetailsStateFactory( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } @@ -74,7 +71,6 @@ internal class TokenDetailsStateFactory( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = tokenDetailsClickIntents, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } 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 778b4f2edf..20d13122a3 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 @@ -5,7 +5,6 @@ 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 -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 @@ -14,7 +13,6 @@ 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 @@ -38,8 +36,6 @@ internal class ExchangeStatusFactory @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, @@ -117,30 +113,22 @@ internal class ExchangeStatusFactory @AssistedInject constructor( 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 accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() - 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 - } + val refundTokenCurrency = if (accountId != null) { + addRefundCurrencyIfNeeded( + accountId = accountId, + status = statusModel, + type = provider.type, + ) } else { - addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null } swapTransactionRepository.storeTransactionState( @@ -170,28 +158,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - /** - * 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( + private suspend fun addRefundCurrencyIfNeeded( accountId: AccountId, status: ExchangeStatusModel?, type: ExchangeProviderType, 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 7d3b0b6008..c5ff9805b6 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 @@ -122,7 +122,8 @@ internal class ExpressStatusFactory @AssistedInject constructor( val analyticEvents = when (expressState) { is ExchangeUM -> listOfNotNull( TokenExchangeAnalyticsEvent.CexTxStatusOpened( - cryptoCurrency.symbol, + token = cryptoCurrency.symbol, + provider = expressState.provider.name, ), maybeGetLongTimeExchangeNotificationShowEvent( expressState = expressState, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt index 7c10c7d5d6..d145a7ab35 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -4,7 +4,6 @@ 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 @@ -13,7 +12,6 @@ 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 @@ -39,8 +37,6 @@ 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, @@ -118,30 +114,22 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( 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 accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() - 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 - } + val refundTokenCurrency = if (accountId != null) { + addRefundCurrencyIfNeeded( + accountId = accountId, + status = statusModel, + type = provider.type, + ) } else { - addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null } swapTransactionRepository.storeTransactionState( @@ -171,28 +159,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( } } - /** - * 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( + private suspend fun addRefundCurrencyIfNeeded( accountId: AccountId, status: ExchangeStatusModel?, type: ExchangeProviderType, 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 index 19387b7150..c40bc24c57 100644 --- 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 @@ -118,7 +118,8 @@ internal class TokenDetailsExpressStatusFactory @AssistedInject constructor( val analyticEvents = when (expressState) { is ExchangeUM -> listOfNotNull( TokenExchangeAnalyticsEvent.CexTxStatusOpened( - cryptoCurrency.symbol, + token = cryptoCurrency.symbol, + provider = expressState.provider.name, ), maybeGetLongTimeExchangeNotificationShowEvent( expressState = expressState, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index abf30bd8f0..a3280e26c4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -148,10 +148,8 @@ internal fun TokenDetailsScreen( ) } - if (state.isYieldSupplyFeatureEnabled) { - item { - yieldSupplyComponent.Content(modifier = itemModifier) - } + item { + yieldSupplyComponent.Content(modifier = itemModifier) } expressTransactionsItems( diff --git a/features/wallet-settings/impl/detekt-baseline-debug.xml b/features/wallet-settings/impl/detekt-baseline-debug.xml deleted file mode 100644 index ecf2e0cce8..0000000000 --- a/features/wallet-settings/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt index 3b7692fe1f..34f48f98ba 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.requestPermission import com.tangem.datasource.local.accounts.AccountTokenMigrationStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -49,7 +48,6 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val networksAvailableForNotificationsComponent: NetworksAvailableForNotificationsComponent.Factory, private val accountTokenMigrationStore: AccountTokenMigrationStore, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : WalletSettingsComponent, AppComponentContext by context { private val model: WalletSettingsModel = getOrCreateModel(params) @@ -74,11 +72,7 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( init { lifecycle.subscribe( - onResume = { - if (accountsFeatureToggles.isFeatureEnabled) { - showMigrationAlertIfNeeded() - } - }, + onResume = { showMigrationAlertIfNeeded() }, onPause = { accountMigrationJobHolder.cancel() }, ) } 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 796d045499..f174c69a07 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 @@ -24,12 +24,10 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -87,7 +85,6 @@ internal class WalletSettingsModel @Inject constructor( private val permissionsRepository: PermissionRepository, private val notificationsRepository: NotificationsRepository, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, @@ -159,8 +156,7 @@ internal class WalletSettingsModel @Inject constructor( accountList = accountList, ), accountReorderUM = AccountReorderUM( - isDragEnabled = accountsFeatureToggles.isFeatureEnabled && - accountList.count { it is WalletSettingsAccountsUM.Account } > 1, + isDragEnabled = accountList.count { it is WalletSettingsAccountsUM.Account } > 1, onMove = ::onAccountReorder, onDragStopped = ::onAccountDragStopped, ), @@ -237,13 +233,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.ManageTokens( source = Source.SETTINGS, - portfolioId = if (accountsFeatureToggles.isFeatureEnabled) { - PortfolioId( - accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId), - ) - } else { - PortfolioId(userWalletId = userWallet.walletId) - }, + accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId), ), ) }, @@ -378,7 +368,7 @@ internal class WalletSettingsModel @Inject constructor( if (!state.value.isWalletBackedUp) { showMakeBackupAtFirstAlertBS() } else { - unlockWalletIfNeedAndProceed { authorizationRequired -> + unlockWalletIfNeedAndProceed { _ -> router.push( route = AppRoute.UpdateAccessCode( userWalletId = params.userWalletId, 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 6c53807de1..56be825aca 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 @@ -12,7 +12,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier @@ -46,13 +45,12 @@ internal class AccountItemsDelegate @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val accountListSortingSaver: AccountListSortingSaver, - private val accountsFeatureToggles: AccountsFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, ) { private val userWalletId = paramsContainer.require().userWalletId - fun isAccountsSupported(wallet: UserWallet) = accountsFeatureToggles.isFeatureEnabled && wallet.isAccountsSupported + fun isAccountsSupported(wallet: UserWallet) = wallet.isAccountsSupported fun loadAccount(wallet: UserWallet): Flow> { if (!isAccountsSupported(wallet)) return flowOf(emptyList()) @@ -145,7 +143,7 @@ internal class AccountItemsDelegate @Inject constructor( return this.sortedBy { positionByAccountId[it.id] ?: Int.MAX_VALUE } } - private fun openAccountDetails(account: Account) { + private fun openAccountDetails(account: Account.CryptoPortfolio) { router.push(AppRoute.AccountDetails(account)) } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 5d647616a7..b7e61208c1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -9,6 +9,12 @@ plugins { android { namespace = "com.tangem.feature.wallet.impl" + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } } dependencies { @@ -44,6 +50,16 @@ dependencies { exclude(group = "com.google.firebase", module = "protolite-well-known-types") exclude(group = "com.google.protobuf", module = "protobuf-javalite") } + implementation(deps.haze) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } + implementation(deps.haze.materials) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } /** DI */ implementation(deps.hilt.android) @@ -84,6 +100,7 @@ dependencies { implementation(projects.domain.nft) implementation(projects.domain.nft.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index 4d05f69171..44d956c114 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -5,11 +5,8 @@ BooleanPropertyNaming:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher$@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem$abstract val showShadow: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem.RoundingMode$abstract val showGap: Boolean - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private var readyForRateAppNotification = false - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$val userHasWalletOrWallet2 = userWallets.filterIsInstance<UserWallet.Cold>().any { val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } BooleanPropertyNaming:OrganizeTokensState.kt$OrganizeTokensState.ActionsConfig$val showApplyProgress: Boolean = false BooleanPropertyNaming:ScrollToWalletTransformer.kt$ScrollToWalletTransformer$private val withScrollAnimation: Boolean = true - BooleanPropertyNaming:SetWalletCardDropDownItemsTransformer.kt$SetWalletCardDropDownItemsTransformer$private val dropdownEnabled: Boolean BooleanPropertyNaming:TangemPayState.kt$TangemPayState.Progress$val showProgress: Boolean = false BooleanPropertyNaming:TokenActionButtonConfig.kt$TokenActionButtonConfig$val enabled: Boolean = true BooleanPropertyNaming:UpdateMultiWalletActionButtonBadgeTransformer.kt$UpdateMultiWalletActionButtonBadgeTransformer$private val showSwapBadge: Boolean @@ -18,127 +15,69 @@ BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Is click enabled */ abstract val enabled: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false - BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false - BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean - BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content && state.tokensListState is WalletTokensListState.ContentState.PortfolioContent BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount > 0 && !listState.canScrollBackward && !listState.canScrollForward || listState.canScrollBackward && !listState.canScrollForward } } BooleanPropertyNaming:WalletScreen.kt$var visible by remember { mutableStateOf(value = false) } BooleanPropertyNaming:WalletScreenState.kt$WalletScreenState$val showMarketsOnboarding: Boolean BooleanPropertyNaming:WalletWithFundsChecker.kt$WalletWithFundsChecker$val prevStatus = statusByWalletId.get(userWalletId) - IgnoredReturnValue:MultiWalletTokenListStore.kt$MultiWalletTokenListStore$remove(userWalletId) MaxChainedCallsOnSameLine:HasSingleWalletSignedHashesUseCase.kt$HasSingleWalletSignedHashesUseCase$userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes - MultilineLambdaItParameter:BasicTokenListSubscriber.kt$BasicTokenListSubscriber${ it.getOrElse { e -> Timber.e("Failed to load app currency: $e") AppCurrency.Default } } MultilineLambdaItParameter:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler${ Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") showAlert(Failed) } MultilineLambdaItParameter:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher${ it.fold( ifLeft = { emit(UserWalletItemUM.ImageState.Loading) }, ifRight = { wallet -> emitAll(walletImage(wallet, size)) }, ) } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) .conflate() .distinctUntilChanged() .firstOrNull() } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } - MultilineLambdaItParameter:NetworkGroupToDraggableItemsConverterV2.kt$NetworkGroupToDraggableItemsConverterV2${ AccountCryptoCurrencyStatus( account = account, status = it, ) } MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ isBalanceHidden = it.isBalanceHidden stateHolder.updateHiddenState(isBalanceHidden) } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSorting(it) cachedTokenList = it } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) cachedAccountStatusList = it } - MultilineLambdaItParameter:OrganizedTokenListConverter.kt$OrganizedTokenListConverter${ AccountCryptoCurrencyStatus( account = cryptoAccount, status = it, ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } - MultilineLambdaItParameter:PrimaryCurrencySubscriberV2.kt$PrimaryCurrencySubscriberV2${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:ReviewManagerRequester.kt$ReviewManagerRequester${ handleOnCompleteRequestTask( reviewManager = reviewManager, activity = context.findActivity(), task = it, onDismissClick = onDismissClick, ) } MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -> when (button) { is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } } - MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } - MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } } MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } } - MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:UpdateMultiWalletActionsTransformer.kt$UpdateMultiWalletActionsTransformer${ when (it) { is WalletManageButton.Buy -> { it.copy( enabled = buyStatus.isContent(), dimContent = !buyStatus.isContent(), ) } is WalletManageButton.Sell -> { it.copy( enabled = sellStatus.isContent(), dimContent = !sellStatus.isContent(), ) } is WalletManageButton.Swap -> { it.copy( enabled = swapStatus.isContent(), dimContent = !swapStatus.isContent(), ) } else -> it } } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get primary currency status $it") null } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get selected wallet $it") null } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e("Unable to get balances and limits: $it") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get transaction details") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get visa currency") return@launch } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load VISA currency") setFailedTxHistoryState(it) return@flow } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load tx history for wallet ${userWallet.walletId}") throw it } MultilineLambdaItParameter:WalletCard.kt${ haptic.performHapticFeedback(HapticFeedbackType.LongPress) isMenuVisible = true pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) } MultilineLambdaItParameter:WalletCard.kt${ val press = PressInteraction.Press(it) interactionSource.emit(press) tryAwaitRelease() interactionSource.emit(PressInteraction.Release(press)) } - MultilineLambdaItParameter:WalletCardClickIntents.kt$WalletCardClickIntentsImplementor${ Timber.e("Unable to delete user wallet: $it") return@launch } - MultilineLambdaItParameter:WalletClickIntents.kt$WalletClickIntents${ if (!it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } walletScreenContentLoader.load( userWallet = it, clickIntents = this@WalletClickIntents, coroutineScope = modelScope, ) } MultilineLambdaItParameter:WalletContentClickIntents.kt$WalletContentClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) return@launch } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } - MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } - MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } MultilineLambdaItParameter:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase${ 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) } MultilineLambdaItParameter:WalletScreen.kt${ PaddingValues( bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, ) } MultilineLambdaItParameter:WalletScreen.kt${ WalletSnackbarHost( snackbarHostState = it, event = state.event, modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing4) .navigationBarsPadding(), ) } - MultilineLambdaItParameter:WalletScreen.kt${ balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, ) } - MultilineLambdaItParameter:WalletScreen.kt${ findPortfolioVisibleState( portfolio = it, expandedState = expandedState, collapsedState = collapsedState, ) } MultilineLambdaItParameter:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) NamedArguments:TangemSnapFlingBehavior.kt$HighVelocityApproachAnimation$animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep) NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( closestOffset, closestOffset, animationState, snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onRemainingScrollOffsetUpdate(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( remainingOffset, remainingOffset, animationState.copy(value = 0f), snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onAnimationStep(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$approach( initialTargetOffset, initialVelocity, animation, snapLayoutInfoProvider, density, onAnimationStep, ) NamedArguments:TangemSnapFlingBehavior.kt$approachAnimation( this, initialTargetOffset, initialVelocity, onAnimationStep, ) - NamedArguments:WalletContent.kt$tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState) NamedArguments:WalletContent.kt$txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) NestedScopeFunctions:WalletScreen.kt$let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } NestedScopeFunctions:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } NestedScopeFunctions:WalletScreen.kt$let { marketPriceBlockState -> marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) } NoNameShadowing:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher${ it.isMultiCurrency } NoNameShadowing:MultiCurrencyAccountContent.kt$modifier - NoNameShadowing:TxHistorySubscriber.kt$TxHistorySubscriber${ it.cachedIn(coroutineScope) } - NoNameShadowing:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ it.cachedIn(coroutineScope) } NoNameShadowing:WalletComponent.kt$WalletComponent$dialog NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${networkStatuses?.size} - NullableToStringCall:WalletStateController.kt$WalletStateController$${transformer::class.simpleName} - NullableToStringCall:WalletSubscriber.kt$WalletSubscriber$${this::class.simpleName} PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_modelScope PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_router PropertyUsedBeforeDeclaration:OrganizeTokensModel.kt$OrganizeTokensModel$uiState PropertyUsedBeforeDeclaration:WalletScreenPreviewData.kt$WalletScreenPreviewData$buyButton PropertyUsedBeforeDeclaration:WalletStateController.kt$WalletStateController$mutableUiState ReusedModifierInstance:DefaultWalletEntryComponent.kt$DefaultWalletEntryComponent$Content(modifier) - ReusedModifierInstance:VisaTxDetailsBottomSheet.kt$LazyColumn( modifier = modifier.background(TangemTheme.colors.background.secondary), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing16, ), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), horizontalAlignment = Alignment.CenterHorizontally, ) { item { TransactionBlock(config.transaction) } items(config.requests) { item -> BlockchainRequestBlock(item) } item { DisputeButton(config.onDisputeClick) } } ReusedModifierInstance:WalletNFTItem.kt$Image( modifier = modifier .background(TangemTheme.colors.stroke.primary), painter = painterResource(R.drawable.ic_nft_preview_more_16), contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$SubcomposeAsyncImage( modifier = modifier, model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, error = { Box( modifier = Modifier.background(TangemTheme.colors.field.primary), ) }, contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$take(modifiers.size) SuspendFunSwallowedCancellation:WalletModel.kt$WalletModel$runCatching - UnnecessaryLet:BalancesAndLimitsBottomSheetConverter.kt$BalancesAndLimitsBottomSheetConverter$let(::formatAmount) - UnnecessaryLet:MultiWalletContentLoader.kt$MultiWalletContentLoader$let(::add) - UnnecessaryLet:SingleWalletWithTokenContentLoader.kt$SingleWalletWithTokenContentLoader$let(::add) UnnecessaryLet:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$let { abs(it) * sign(initialVelocity) // ensure offset sign is correct } UnnecessaryLet:WalletClickIntents.kt$WalletClickIntents$let(::add) UnnecessaryLet:WalletScreen.kt$let { (state.tokensListState as? WalletTokensListState.ContentState)?.let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } } UnnecessaryLet:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - UnnecessarySafeCall:SetVisaInfoTransformer.kt$SetVisaInfoTransformer$visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) } UseEmptyCounterpart:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher$mapOf<String, ArtworkUM>() - UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$mapOf() UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$setOf() UseEmptyCounterpart:PortfolioOrganizeTokensAnalyticsEvent.kt$PortfolioOrganizeTokensAnalyticsEvent$mapOf() UseEmptyCounterpart:PromoActivationAnalytics.kt$PromoActivationAnalytics$mapOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber$listOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriberV2.kt$SingleWalletExpressStatusesSubscriberV2$listOf() UseEmptyCounterpart:TokenListStateConverter.kt$TokenListStateConverter$listOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.Basic$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.MainScreen$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.PushBannerPromo$mapOf() - UseOrEmpty:CryptoCurrenciesIdsResolver.kt$CryptoCurrenciesIdsResolver$accountStatusList?.accountStatuses ?.filter { it.getCryptoTokenList() != TokenList.Empty } ?.associate { accountStatus -> val currencies = accountStatus.flattenCurrencies() accountStatus.account as Account.CryptoPortfolio to draggableTokens .asSequence() .filter { it.accountId == accountStatus.account.accountId.value } .mapNotNull { sortedToken -> currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency } .toList() } ?: emptyMap() UseSumOfInsteadOfFlatMapSize:TokenListStateConverter.kt$TokenListStateConverter$flatMap(NetworkGroup::currencies) VarCouldBeVal:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$private var motionScaleDuration = DefaultScrollMotionDurationScale - VarCouldBeVal:WalletModel.kt$WalletModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/DefaultWalletEntryComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/DefaultWalletEntryComponent.kt index d4555ccf75..bd970a6b69 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/DefaultWalletEntryComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/DefaultWalletEntryComponent.kt @@ -14,7 +14,7 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponentLegacy import com.tangem.feature.wallet.child.wallet.WalletComponent import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.features.wallet.WalletEntryComponent @@ -40,9 +40,9 @@ internal class DefaultWalletEntryComponent @AssistedInject constructor( appComponentContext = childByContext(context), navigate = { navigation.pushNew(it) }, ) - is WalletRoute.OrganizeTokens -> OrganizeTokensComponent( + is WalletRoute.OrganizeTokens -> OrganizeTokensComponentLegacy( appComponentContext = childByContext(context), - params = OrganizeTokensComponent.Params(route.userWalletId), + params = OrganizeTokensComponentLegacy.Params(route.userWalletId), onBack = { navigation.pop() }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponentLegacy.kt similarity index 82% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponentLegacy.kt index b54ed3161b..7c6c23fa29 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponentLegacy.kt @@ -8,17 +8,17 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen +import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModelLegacy +import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensScreen import kotlinx.coroutines.launch -internal class OrganizeTokensComponent( +internal class OrganizeTokensComponentLegacy( appComponentContext: AppComponentContext, params: Params, onBack: () -> Unit, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OrganizeTokensModel = getOrCreateModel(params) + private val model: OrganizeTokensModelLegacy = getOrCreateModel(params) init { componentScope.launch { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt similarity index 92% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt index a0c96c2974..5d4948ab02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.analytics +package com.tangem.feature.wallet.child.organizetokens.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt new file mode 100644 index 0000000000..e27eb472ed --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.child.organizetokens.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModelLegacy +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface OrganizeTokensModule { + + @Binds + @IntoMap + @ClassKey(OrganizeTokensModelLegacy::class) + fun bindOrganizeTokensModelLegacy(model: OrganizeTokensModelLegacy): Model +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt similarity index 60% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt index da7e0746a2..a2c6430e52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState @@ -11,13 +11,13 @@ import com.tangem.feature.wallet.impl.R * Helper class for the DND list items * * @property id ID of the item - * @property roundingMode item [RoundingMode] + * @property roundingModeUM item [RoundingModeUM] * @property showShadow if true then item should be elevated * */ @Immutable internal sealed class DraggableItem { abstract val id: Any - abstract val roundingMode: RoundingMode + abstract val roundingModeUM: RoundingModeUM abstract val showShadow: Boolean /** @@ -26,14 +26,14 @@ internal sealed class DraggableItem { * @property id ID of the network group * @property networkName network group name * @property accountId account id - * @property roundingMode item [RoundingMode] + * @property roundingModeUM item [RoundingModeUM] * @property showShadow if true then item should be elevated * */ data class GroupHeader( override val id: Int, val networkName: String, val accountId: String = "", - override val roundingMode: RoundingMode = RoundingMode.None, + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, override val showShadow: Boolean = false, ) : DraggableItem() { @@ -53,7 +53,7 @@ internal sealed class DraggableItem { * @property groupId ID of the network group which contains this token * @property accountId account id * @property id ID of the token - * @property roundingMode item [RoundingMode] + * @property roundingModeUM item [RoundingModeUM] * @property showShadow if true then item should be elevated * */ data class Token( @@ -61,7 +61,7 @@ internal sealed class DraggableItem { val groupId: Int, val accountId: String = "", override val showShadow: Boolean = false, - override val roundingMode: RoundingMode = RoundingMode.None, + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, ) : DraggableItem() { override val id: String = tokenItemState.id } @@ -77,7 +77,7 @@ internal sealed class DraggableItem { val accountId: String = "", ) : DraggableItem() { override val showShadow: Boolean = false - override val roundingMode: RoundingMode = RoundingMode.None + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None } /** @@ -85,11 +85,11 @@ internal sealed class DraggableItem { * * @property tokenItemState state of the portfolio item * @property id ID of the portfolio - * @property roundingMode item [RoundingMode] + * @property roundingModeUM item [RoundingModeUM] * @property showShadow if true then item should be elevated * */ data class Portfolio( - override val roundingMode: RoundingMode = RoundingMode.None, + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, val tokenItemState: TokenItemState, ) : DraggableItem() { override val id: String = tokenItemState.id @@ -97,55 +97,17 @@ internal sealed class DraggableItem { } /** - * Rounding mode of the [DraggableItem] + * Update item [RoundingModeUM] * - * @property showGap if true then item should have padding on rounded side - * */ - @Immutable - sealed class RoundingMode { - abstract val showGap: Boolean - - /** - * In this mode, item is not rounded - * */ - object None : RoundingMode() { - override val showGap: Boolean = false - } - - /** - * In this mode, item should have a rounded top side - * - * @property showGap if true then item should have top padding - * */ - data class Top(override val showGap: Boolean = false) : RoundingMode() - - /** - * In this mode, item should have a rounded bottom side - * - * @property showGap if true then item should have bottom padding - * */ - data class Bottom(override val showGap: Boolean = false) : RoundingMode() - - /** - * In this mode, item should have a rounded all sides - * - * @property showGap if true then item should have top and bottom padding - * */ - data class All(override val showGap: Boolean = false) : RoundingMode() - } - - /** - * Update item [RoundingMode] - * - * @param mode new [RoundingMode] + * @param mode new [RoundingModeUM] * * @return updated [DraggableItem] * */ - fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) { + fun updateRoundingMode(mode: RoundingModeUM): DraggableItem = when (this) { is Placeholder -> this - is Portfolio -> this.copy(roundingMode = mode) - is GroupHeader -> this.copy(roundingMode = mode) - is Token -> this.copy(roundingMode = mode) + is Portfolio -> this.copy(roundingModeUM = mode) + is GroupHeader -> this.copy(roundingModeUM = mode) + is Token -> this.copy(roundingModeUM = mode) } /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt similarity index 54% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt index 720edbb802..38144081c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt @@ -1,27 +1,9 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -@Deprecated("Use OrganizeTokensListUM instead, will be removed in future releases") -@Immutable -internal sealed class OrganizeTokensListState { - abstract val items: PersistentList - - data class GroupedByNetwork( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data class Ungrouped( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data object Empty : OrganizeTokensListState() { - override val items: PersistentList = persistentListOf() - } -} - @Immutable internal sealed interface OrganizeTokensListUM { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt index 556f2e081d..234ab0e793 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent @@ -7,7 +7,6 @@ import org.burnoutcrew.reorderable.ItemPosition @Immutable internal data class OrganizeTokensState( val onBackClick: () -> Unit, - val itemsState: OrganizeTokensListState, val tokenListUM: OrganizeTokensListUM, val header: HeaderConfig, val actions: ActionsConfig, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/RoundingModeUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/RoundingModeUM.kt new file mode 100644 index 0000000000..f21b84f36e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/RoundingModeUM.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.child.organizetokens.entity + +import androidx.compose.runtime.Immutable + +/** + * Rounding mode of the [DraggableItem] + * + * @property isShowGap if true then item should have padding on rounded side + * */ +@Immutable +internal sealed class RoundingModeUM { + abstract val isShowGap: Boolean + + /** + * In this mode, item is not rounded + * */ + object None : RoundingModeUM() { + override val isShowGap: Boolean = false + } + + /** + * In this mode, item should have a rounded top side + * + * @property isShowGap if true then item should have top padding + * */ + data class Top(override val isShowGap: Boolean = false) : RoundingModeUM() + + /** + * In this mode, item should have a rounded bottom side + * + * @property isShowGap if true then item should have bottom padding + * */ + data class Bottom(override val isShowGap: Boolean = false) : RoundingModeUM() + + /** + * In this mode, item should have a rounded all sides + * + * @property isShowGap if true then item should have top and bottom padding + * */ + data class All(override val isShowGap: Boolean = false) : RoundingModeUM() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt new file mode 100644 index 0000000000..cda6bbe86c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencies +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM + +internal class CryptoCurrenciesIdsResolver { + + fun resolveLegacy( + tokensListUM: OrganizeTokensListUM, + accountStatusList: AccountStatusList?, + ): AccountCryptoCurrencies { + if (accountStatusList == null) return emptyMap() + + val draggableTokens = when (tokensListUM) { + OrganizeTokensListUM.EmptyList -> return emptyMap() + is OrganizeTokensListUM.AccountList, + is OrganizeTokensListUM.TokensList, + -> tokensListUM.items.filterIsInstance() + } + + return accountStatusList.accountStatuses + .filterCryptoPortfolio() + .filter { it.tokenList != TokenList.Empty } + .associate { accountStatus -> + val currenciesById = accountStatus.flattenCurrencies().associateBy { it.currency.id.value } + + accountStatus.account to draggableTokens + .asSequence() + .filter { it.accountId == accountStatus.account.accountId.value } + .mapNotNull { token -> currenciesById[token.id]?.currency } + .toList() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt similarity index 64% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt index e0f4868347..48dc8f24ca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt @@ -1,6 +1,7 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.model -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import androidx.compose.runtime.Stable +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import org.burnoutcrew.reorderable.ItemPosition internal interface OrganizeTokensIntents { @@ -16,13 +17,14 @@ internal interface OrganizeTokensIntents { fun onCancelClick() } +@Stable internal interface DragAndDropIntents { fun onItemDragged(from: ItemPosition, to: ItemPosition) fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean - fun onItemDraggingStart(item: DraggableItem) + fun onItemDraggingStartLegacy(item: DraggableItem) fun onItemDraggingEnd() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt deleted file mode 100644 index fb310ec8be..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ /dev/null @@ -1,338 +0,0 @@ -package com.tangem.feature.wallet.child.organizetokens.model - -import androidx.compose.runtime.Stable -import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -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.account.status.usecase.ApplyTokenListSortingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCaseV2 -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.core.lce.Lce -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase -import com.tangem.domain.tokens.ToggleTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder -import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import javax.inject.Inject - -@Suppress("LongParameterList") -@Stable -@ModelScoped -internal class OrganizeTokensModel @Inject constructor( - paramsContainer: ParamsContainer, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - override val dispatchers: CoroutineDispatcherProvider, - private val getTokenListUseCase: GetTokenListUseCase, - private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, - private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventsHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val toggleTokenListGroupingUseCaseV2: ToggleTokenListGroupingUseCaseV2, - private val toggleTokenListSortingUseCaseV2: ToggleTokenListSortingUseCaseV2, - private val applyTokenListSortingUseCaseV2: ApplyTokenListSortingUseCaseV2, -) : Model(), OrganizeTokensIntents { - - private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() - - private var isBalanceHidden = true - - private val dragAndDropAdapter = DragAndDropAdapter( - listStateProvider = Provider { uiState.value.itemsState }, - ) - - private val dragAndDropAdapterV2 = DragAndDropAdapterV2( - tokenListUMProvider = Provider { uiState.value.tokenListUM }, - ) - - private val stateHolder = OrganizeTokensStateHolder( - intents = this, - dragAndDropIntents = dragAndDropAdapter, - dragAndDropAdapterV2 = dragAndDropAdapterV2, - appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - accountsFeatureToggles = accountsFeatureToggles, - ) - - private val userWalletId = paramsContainer.require().userWalletId - - private var cachedTokenList: TokenList? = null - private var cachedAccountStatusList: AccountStatusList? = null - - private var isAccountsModeEnabled: Boolean = false - - val uiState: StateFlow = stateHolder.stateFlow - - val onBack = MutableSharedFlow() - - init { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened()) - - getBalanceHidingSettingsUseCase() - .onEach { - isBalanceHidden = it.isBalanceHidden - stateHolder.updateHiddenState(isBalanceHidden) - } - .launchIn(modelScope) - - bootstrapTokenList() - bootstrapDragAndDropUpdates() - } - - override fun onBackClick() { - modelScope.launch { onBack.emit(Unit) } - } - - override fun onSortClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return - if (list.sortType == TokensSortType.BALANCE) return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - - modelScope.launch { - toggleTokenListSortingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - if (list.sortedBy == TokensSortType.BALANCE) return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - - modelScope.launch { - toggleTokenListSortingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } - } - } - - override fun onGroupClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - - modelScope.launch { - toggleTokenListGroupingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - - modelScope.launch { - toggleTokenListGroupingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } - } - } - - override fun onApplyClick() { - modelScope.launch { - stateHolder.updateStateToDisplayProgress() - val resolver = CryptoCurrenciesIdsResolver() - val isSortedByBalance = uiState.value.header.isSortedByBalance - - val result = if (accountsFeatureToggles.isFeatureEnabled) { - val tokensListUM = uiState.value.tokenListUM - - val isGroupedByNetwork = tokensListUM.isGrouped - - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCaseV2( - sortedTokensIdsByAccount = resolver.resolveV2(tokensListUM, cachedAccountStatusList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } else { - val listState = uiState.value.itemsState - - val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork - - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCase( - userWalletId = userWalletId, - sortedTokensIds = resolver.resolve(listState, cachedTokenList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } - - result.fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - modelScope.launch { onBack.emit(Unit) } - stateHolder.updateStateToHideProgress() - }, - ) - } - } - - override fun onCancelClick() { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel()) - - modelScope.launch { onBack.emit(Unit) } - } - - private fun bootstrapTokenList() { - modelScope.launch { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountList = singleAccountStatusListSupplier.getSyncOrNull( - SingleAccountStatusListProducer.Params(userWalletId), - ) ?: return@launch - - isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() - - stateHolder.updateStateWithAccountList( - accountStatusList = accountList, - isAccountsModeEnabled = isAccountsModeEnabled, - ) - - cachedAccountStatusList = accountList - } else { - val tokenList = getTokenList() ?: return@launch - stateHolder.updateStateWithTokenList(tokenList) - cachedTokenList = tokenList - } - } - } - - private suspend fun getTokenList(): TokenList? { - val maybeTokenList = getTokenListUseCase.launch(userWalletId) - .filterNot(Lce::isLoading) - .firstOrNull() - ?: return null - - return maybeTokenList - .onError(stateHolder::updateStateWithError) - .getOrNull(isPartialContentAccepted = false) - } - - private fun bootstrapDragAndDropUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - dragAndDropAdapterV2.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChangedV2(type) - - stateHolder.updateStateWithManualSortingV2(updatedListState) - } - .launchIn(modelScope) - } else { - dragAndDropAdapter.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChanged(type) - - stateHolder.updateStateWithManualSorting(updatedListState) - } - .launchIn(modelScope) - } - } - - private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { - if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return - - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { - cachedTokenList = cachedTokenList?.disableSortingByBalance() - stateHolder.disableSortingByBalance() - } - } - - private fun disableSortingByBalanceIfListChangedV2(dragOperationType: DragAndDropAdapterV2.DragOperation.Type) { - if (dragOperationType !is DragAndDropAdapterV2.DragOperation.Type.End) return - - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { - cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) - stateHolder.disableSortingByBalance() - } - } - - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - } - - private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) { - analyticsEventsHandler.send( - PortfolioOrganizeTokensAnalyticsEvent.Apply( - grouping = if (isGroupedByNetwork) { - AnalyticsParam.OnOffState.On - } else { - AnalyticsParam.OnOffState.Off - }, - organizeSortType = if (isSortedByBalance) { - AnalyticsParam.OrganizeSortType.ByBalance - } else { - AnalyticsParam.OrganizeSortType.Manually - }, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt new file mode 100644 index 0000000000..5a9359c88e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt @@ -0,0 +1,224 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase +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.models.TokensSortType +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponentLegacy +import com.tangem.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapterLegacy +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class OrganizeTokensModelLegacy @Inject constructor( + paramsContainer: ParamsContainer, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + override val dispatchers: CoroutineDispatcherProvider, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, + private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, + private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, +) : Model(), OrganizeTokensIntents { + + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private var isBalanceHidden = true + + @Suppress("PropertyUsedBeforeDeclaration") + private val dragAndDropAdapterLegacy = DragAndDropAdapterLegacy( + tokenListUMProvider = Provider { uiState.value.tokenListUM }, + ) + + private val stateHolder = OrganizeTokensStateHolder( + intents = this, + dragAndDropAdapterLegacy = dragAndDropAdapterLegacy, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ) + + private val userWalletId = paramsContainer.require().userWalletId + + private var cachedAccountStatusList: AccountStatusList? = null + + private var isAccountsModeEnabled: Boolean = false + + val uiState: StateFlow = stateHolder.stateFlow + + val onBack = MutableSharedFlow() + + init { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened()) + + getBalanceHidingSettingsUseCase() + .onEach { balanceHidingSettings -> + isBalanceHidden = balanceHidingSettings.isBalanceHidden + stateHolder.updateHiddenState(isBalanceHidden) + } + .launchIn(modelScope) + + bootstrapTokenList() + bootstrapDragAndDropUpdates() + } + + override fun onBackClick() { + modelScope.launch { onBack.emit(Unit) } + } + + override fun onSortClick() { + val list = cachedAccountStatusList ?: return + if (list.sortType == TokensSortType.BALANCE) return + + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) + + modelScope.launch { + toggleTokenListSortingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) + } + } + + override fun onGroupClick() { + val list = cachedAccountStatusList ?: return + + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) + + modelScope.launch { + toggleTokenListGroupingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) + } + } + + override fun onApplyClick() { + modelScope.launch { + stateHolder.updateStateToDisplayProgress() + val resolver = CryptoCurrenciesIdsResolver() + val isSortedByBalance = uiState.value.header.isSortedByBalance + val tokensListUM = uiState.value.tokenListUM + + val isGroupedByNetwork = tokensListUM.isGrouped + + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + val result = applyTokenListSortingUseCase( + sortedTokensIdsByAccount = resolver.resolveLegacy(tokensListUM, cachedAccountStatusList), + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + result.fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + modelScope.launch { onBack.emit(Unit) } + stateHolder.updateStateToHideProgress() + }, + ) + } + } + + override fun onCancelClick() { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel()) + + modelScope.launch { onBack.emit(Unit) } + } + + private fun bootstrapTokenList() { + modelScope.launch { + val accountList = singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return@launch + + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + + stateHolder.updateStateWithAccountList( + accountStatusList = accountList, + isAccountsModeEnabled = isAccountsModeEnabled, + ) + + cachedAccountStatusList = accountList + } + } + + private fun bootstrapDragAndDropUpdates() { + dragAndDropAdapterLegacy.dragAndDropUpdates + .distinctUntilChanged() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChanged(type) + + stateHolder.updateStateWithManualSorting(updatedListState) + } + .launchIn(modelScope) + } + + private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapterLegacy.DragOperation.Type) { + if (dragOperationType !is DragAndDropAdapterLegacy.DragOperation.Type.End) return + + if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { + cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) + stateHolder.disableSortingByBalance() + } + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) { + analyticsEventsHandler.send( + PortfolioOrganizeTokensAnalyticsEvent.Apply( + grouping = if (isGroupedByNetwork) { + AnalyticsParam.OnOffState.On + } else { + AnalyticsParam.OnOffState.Off + }, + organizeSortType = if (isSortedByBalance) { + AnalyticsParam.OrganizeSortType.ByBalance + } else { + AnalyticsParam.OrganizeSortType.Manually + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt new file mode 100644 index 0000000000..9a205a7735 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.TokenListToStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.error.TokenListSortingErrorConverter +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapterLegacy +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +internal class OrganizeTokensStateHolder( + private val intents: OrganizeTokensIntents, + private val dragAndDropAdapterLegacy: DragAndDropAdapterLegacy, + private val appCurrencyProvider: Provider, +) { + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val inProgressStateConverter by lazy { InProgressStateConverter() } + + private val tokenListSortingErrorConverter by lazy { + TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) + } + + val stateFlow: StateFlow = stateFlowInternal + + fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this) + } + } + + fun updateStateAfterTokenListSorting(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this).copy( + scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), + ) + } + } + + fun updateStateToDisplayProgress() { + updateState { inProgressStateConverter.convert(value = this) } + } + + fun updateStateToHideProgress() { + updateState { inProgressStateConverter.convertBack(value = this) } + } + + fun updateStateWithManualSorting(tokenListUM: OrganizeTokensListUM) { + updateState { copy(tokenListUM = tokenListUM) } + } + + fun disableSortingByBalance() { + updateState { copy(header = header.copy(isSortedByBalance = false)) } + } + + fun updateHiddenState(isBalanceHidden: Boolean) { + updateState { copy(isBalanceHidden = isBalanceHidden) } + } + + fun updateStateWithError(error: TokenListSortingError) { + updateState { tokenListSortingErrorConverter.convert(error) } + } + + private fun getInitialState(): OrganizeTokensState { + return OrganizeTokensState( + onBackClick = intents::onBackClick, + tokenListUM = OrganizeTokensListUM.EmptyList, + header = OrganizeTokensState.HeaderConfig( + onSortClick = intents::onSortClick, + onGroupClick = intents::onGroupClick, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = intents::onApplyClick, + onCancelClick = intents::onCancelClick, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = dragAndDropAdapterLegacy::onItemDragged, + onItemDragStart = dragAndDropAdapterLegacy::onItemDraggingStartLegacy, + onItemDragEnd = dragAndDropAdapterLegacy::onItemDraggingEnd, + canDragItemOver = dragAndDropAdapterLegacy::canDragItemOver, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + stateFlowInternal.update(block) + } + + private fun consumeScrollListToTopEvent() { + updateState { copy(scrollListToTop = consumedEvent()) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt new file mode 100644 index 0000000000..28cfeba4d9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.child.organizetokens.model.common + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem + +internal fun getGroupPlaceholderLegacy(index: Int, accountId: String = ""): DraggableItem.Placeholder { + return DraggableItem.Placeholder( + id = "placeholder_${accountId}_${index.inc()}", + accountId = accountId, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt new file mode 100644 index 0000000000..5735d4ee77 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt @@ -0,0 +1,113 @@ +package com.tangem.feature.wallet.child.organizetokens.model.common + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM + +internal fun List.uniteItemsLegacy(isAccountsMode: Boolean): List { + val items = this + val lastItemIndex = items.lastIndex + + return items + .asSequence() + .mapIndexed { index, item -> + val mode = when (index) { + 0 -> if (item is DraggableItem.Placeholder) { + RoundingModeUM.None + } else { + RoundingModeUM.Top() + } + lastItemIndex -> RoundingModeUM.Bottom() + 1 -> if (items.first() is DraggableItem.Placeholder) { + RoundingModeUM.Top() + } else { + RoundingModeUM.None + } + else -> when (item) { + is DraggableItem.Placeholder -> RoundingModeUM.None + is DraggableItem.GroupHeader -> if (isAccountsMode) { + RoundingModeUM.None + } else { + RoundingModeUM.Top(isShowGap = true) + } + is DraggableItem.Token -> applyRoundingModeToTokenLegacy( + isAccountsMode = isAccountsMode, + items = items, + index = index, + lastItemIndex = lastItemIndex, + ) + is DraggableItem.Portfolio -> RoundingModeUM.Top(isShowGap = true) + } + } + + item + .updateRoundingMode(mode) + .updateShadowVisibility(show = false) + }.toList() +} + +internal fun List.divideMovingItem(movingItem: DraggableItem): List { + val mutableList = this.toMutableList() + val listIterator = mutableList.listIterator() + + while (listIterator.hasNext()) { + val item = listIterator.next() + + if (item.id == movingItem.id) { + val dividedItem = movingItem + .updateRoundingMode(RoundingModeUM.All()) + .updateShadowVisibility(show = true) + + listIterator.set(dividedItem) + break + } + } + + return mutableList +} + +/** + * Applying rounding to tokens + * + * If is in accounts mode without grouping + * * PORTFOLIO + * * TOKEN + * * TOKEN <- add rounding + * * PORTFOLIO index + 1 is PORTFOLIO + * + * If is in accounts mode with grouping + * * PORTFOLIO + * * PLACEHOLDER + * * GROUPING + * * TOKEN + * * TOKEN <- add rounding + * * PLACEHOLDER index + 1 is PLACEHOLDER + * * PORTFOLIO index + 2 is PORTFOLIO + * * PLACEHOLDER + * + * If is not accounts mode without grouping + * * TOKEN + * * TOKEN <- add rounding + * + * If is not accounts mode with grouping + * * PLACEHOLDER + * * GROUPING + * * TOKEN + * * TOKEN <- add rounding + * * PLACEHOLDER index + 1 is PLACEHOLDER + */ +private fun applyRoundingModeToTokenLegacy( + isAccountsMode: Boolean, + items: List, + index: Int, + lastItemIndex: Int, +) = when { + isAccountsMode && index + 1 < lastItemIndex && + (items[index + 1] is DraggableItem.Portfolio || + items[index + 1] is DraggableItem.Placeholder && items[index + 2] is DraggableItem.Portfolio) -> { + RoundingModeUM.Bottom(isShowGap = true) + } + (!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is DraggableItem.Placeholder -> { + RoundingModeUM.Bottom(isShowGap = true) + } + else -> RoundingModeUM.None +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt index 0225b0d6ca..b4d70c5fb0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt new file mode 100644 index 0000000000..2935a14855 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.child.organizetokens.model.common + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal inline fun OrganizeTokensListUM.updateItems( + update: (PersistentList) -> List, +): OrganizeTokensListUM { + val updatedItems = update(items).toPersistentList() + + return when (this) { + is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) + is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) + OrganizeTokensListUM.EmptyList -> this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt similarity index 82% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt index 936385e8ea..7da5b51e59 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState import com.tangem.utils.converter.TwoWayConverter internal class InProgressStateConverter : TwoWayConverter { 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/child/organizetokens/model/converter/TokenListToStateConverter.kt similarity index 73% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt index 279d59f8ed..2d1b76c423 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/child/organizetokens/model/converter/TokenListToStateConverter.kt @@ -1,24 +1,23 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.domain.account.models.AccountStatusList 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.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 -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.OrganizedTokenListConverter +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholderLegacy +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItemsLegacy +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizedTokenListConverter import com.tangem.utils.converter.Converter import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList -internal class TokenListToStateConverterV2( +internal class TokenListToStateConverter( private val accountStatusList: AccountStatusList, private val isAccountsMode: Boolean, private val appCurrency: AppCurrency, @@ -70,11 +69,16 @@ internal class AccountTokenItemConverter( tokenItemState = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = accountStatus.account, - ).convert(TotalFiatBalance.Loading), + ).convert(accountStatus.tokenList.totalFiatBalance), ), ) if (isGrouping) { - add(getGroupPlaceholder(accountId = accountStatus.accountId.value, index = -1)) + add( + getGroupPlaceholderLegacy( + accountId = accountStatus.accountId.value, + index = -1, + ), + ) } addAll(organizedTokenListConverter.convert(accountStatus)) } @@ -82,17 +86,17 @@ internal class AccountTokenItemConverter( emptyList() } }.toList() - .uniteItemsV2(true).toPersistentList(), + .uniteItemsLegacy(true).toPersistentList(), ) } else { OrganizeTokensListUM.TokensList( isGrouped = isGrouping, items = buildList { if (isGrouping) { - add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1)) + add(getGroupPlaceholderLegacy(accountId = value.mainAccount.accountId.value, index = -1)) } addAll(organizedTokenListConverter.convert(value.mainAccount)) - }.uniteItemsV2(false) + }.uniteItemsLegacy(false) .toPersistentList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt similarity index 65% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt index 7d7bcd16c5..f86c07468e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error +package com.tangem.feature.wallet.child.organizetokens.model.converter.error import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter 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/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 478351d5dc..cd6c995427 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/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState @@ -11,14 +11,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance 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 +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import java.math.BigDecimal -internal class CryptoCurrencyToDraggableItemConverterV2( +internal class CryptoCurrencyToDraggableItemConverter( private val appCurrency: AppCurrency, ) : Converter { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt similarity index 73% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt index 13206c8a13..5023751eca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -1,16 +1,16 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -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.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholderLegacy import com.tangem.utils.converter.Converter -internal class NetworkGroupToDraggableItemsConverterV2( - private val itemConverter: CryptoCurrencyToDraggableItemConverterV2, +internal class NetworkGroupToDraggableItemsConverter( + private val itemConverter: CryptoCurrencyToDraggableItemConverter, ) : Converter, List> { override fun convert(value: Pair): List { @@ -28,7 +28,7 @@ internal class NetworkGroupToDraggableItemsConverterV2( convert(pair).toMutableList() .also { mutableGroup -> mutableGroup.add( - getGroupPlaceholder(accountId = pair.first.accountId.value, index = index), + getGroupPlaceholderLegacy(accountId = pair.first.accountId.value, index = index), ) } } @@ -42,10 +42,10 @@ internal class NetworkGroupToDraggableItemsConverterV2( private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List { return itemConverter.convertList( - group.currencies.map { + group.currencies.map { currencyStatus -> AccountCryptoCurrencyStatus( account = account, - status = it, + status = currencyStatus, ) }, ) 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/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt index c0938d2748..c1f5ce4fef 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/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt @@ -1,10 +1,10 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -14,9 +14,9 @@ internal class OrganizedTokenListConverter( private val appCurrency: AppCurrency, ) : Converter> { - private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } + private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverter(appCurrency) } private val groupsConverter by lazy { - NetworkGroupToDraggableItemsConverterV2(tokensConverter) + NetworkGroupToDraggableItemsConverter(tokensConverter) } override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt index 6c0a7531b8..f6de70d6e6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt @@ -1,11 +1,11 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd +package com.tangem.feature.wallet.child.organizetokens.model.dnd -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItemsLegacy +import com.tangem.feature.wallet.child.organizetokens.model.common.updateItems import com.tangem.utils.Provider import kotlinx.collections.immutable.mutate import kotlinx.coroutines.flow.Flow @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import org.burnoutcrew.reorderable.ItemPosition -internal class DragAndDropAdapterV2( +internal class DragAndDropAdapterLegacy( private val tokenListUMProvider: Provider, ) : DragAndDropIntents { @@ -72,7 +72,7 @@ internal class DragAndDropAdapterV2( return canDrag } - override fun onItemDraggingStart(item: DraggableItem) { + override fun onItemDraggingStartLegacy(item: DraggableItem) { if (draggingItem != null) return draggingItem = item @@ -81,7 +81,7 @@ internal class DragAndDropAdapterV2( is DraggableItem.Placeholder, is DraggableItem.Portfolio, -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item) + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupLegacy(items, item) .divideMovingItem(item) is DraggableItem.Token -> items.divideMovingItem(item) } @@ -96,11 +96,11 @@ internal class DragAndDropAdapterV2( updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { when (draggingItem) { is DraggableItem.GroupHeader -> { - draggableGroupsOperations.expandGroupsV2(items) - .uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + draggableGroupsOperations.expandGroupsLegacy(items) + .uniteItemsLegacy(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Token -> { - items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + items.uniteItemsLegacy(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Placeholder, is DraggableItem.Portfolio, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt new file mode 100644 index 0000000000..6487f86b78 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt @@ -0,0 +1,70 @@ +package com.tangem.feature.wallet.child.organizetokens.model.dnd + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholderLegacy + +internal class DraggableGroupsOperations { + + private var groupIdToTokensLegacy: Map>? = null + + fun collapseGroupLegacy(items: List, movingGroup: DraggableItem.GroupHeader): List { + if (!groupIdToTokensLegacy.isNullOrEmpty()) return items + + groupIdToTokensLegacy = items + .asSequence() + .filterIsInstance() + .groupBy { it.groupId } + + val itemsWithoutGroupTokens = items.filterNot { + it is DraggableItem.Token && it.groupId == movingGroup.id + } + + return itemsWithoutGroupTokens.divideMovingItem(movingGroup) + } + + fun expandGroupsLegacy(items: List): List { + if (groupIdToTokensLegacy.isNullOrEmpty()) return items + + val accountList = items.filterIsInstance() + val currentGroups = items.filterIsInstance() + + val expandedGroups = if (items.any { it is DraggableItem.Portfolio }) { + accountList + .asSequence() + .flatMap { account -> + buildList { + add(account) + currentGroups + .asSequence() + .filter { it.accountId == account.id } + .forEachIndexed { index, group -> + if (index == 0) { + add(getGroupPlaceholderLegacy(accountId = group.accountId, index = -1)) + } + add(group) + addAll(groupIdToTokensLegacy?.get(group.id).orEmpty()) + add(getGroupPlaceholderLegacy(accountId = group.accountId, index = index)) + } + } + } + } else { + currentGroups + .asSequence() + .flatMapIndexed { index, group -> + buildList { + if (index == 0) { + add(getGroupPlaceholderLegacy(accountId = group.accountId, index = -1)) + } + add(group) + addAll(groupIdToTokensLegacy?.get(group.id).orEmpty()) + add(getGroupPlaceholderLegacy(accountId = group.accountId, index = index)) + } + } + }.toList() + + groupIdToTokensLegacy = null + + return expandedGroups + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt index 2996d749be..a9ba023093 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler @@ -44,12 +44,12 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.OrganizeTokensScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM +import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreviewLegacy import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable @@ -76,7 +76,6 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier .fillMaxSize(), listState = tokensListState, tokensListUM = state.tokenListUM, - state = state.itemsState, dndConfig = state.dndConfig, isBalanceHidden = state.isBalanceHidden, ) @@ -98,18 +97,13 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier @Composable private fun TokenList( listState: LazyListState, - state: OrganizeTokensListState, tokensListUM: OrganizeTokensListUM, dndConfig: OrganizeTokensState.DragAndDropConfig, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val tokenList = if (tokensListUM !is OrganizeTokensListUM.EmptyList) { - tokensListUM.items - } else { - state.items - } + val tokenList = tokensListUM.items Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { { _, _ -> @@ -188,7 +182,7 @@ private fun LazyItemScope.DraggableItem( mutableStateOf(value = false) } - val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) + val itemModifier = Modifier.applyShapeAndShadow(item.roundingModeUM, item.showShadow) ReorderableItem( reorderableState = reorderableState, @@ -342,14 +336,14 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie } } -private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier { +private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShadow: Boolean): Modifier { return composed { val radius by animateDpAsState( targetValue = when (roundingMode) { - is DraggableItem.RoundingMode.None -> TangemTheme.dimens.radius0 - is DraggableItem.RoundingMode.All -> TangemTheme.dimens.radius12 - is DraggableItem.RoundingMode.Bottom, - is DraggableItem.RoundingMode.Top, + is RoundingModeUM.None -> TangemTheme.dimens.radius0 + is RoundingModeUM.All -> TangemTheme.dimens.radius12 + is RoundingModeUM.Bottom, + is RoundingModeUM.Top, -> TangemTheme.dimens.radius16 }, label = "item_shape_radius", @@ -375,15 +369,15 @@ private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMod @Composable @ReadOnlyComposable -private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues { +private fun getItemGap(roundingMode: RoundingModeUM): PaddingValues { val paddingValue = TangemTheme.dimens.spacing4 - return if (roundingMode.showGap) { + return if (roundingMode.isShowGap) { when (roundingMode) { - is DraggableItem.RoundingMode.None -> PaddingValues(all = 0.dp) - is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue) - is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue) - is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue) + is RoundingModeUM.None -> PaddingValues(all = 0.dp) + is RoundingModeUM.All -> PaddingValues(vertical = paddingValue) + is RoundingModeUM.Top -> PaddingValues(top = paddingValue) + is RoundingModeUM.Bottom -> PaddingValues(bottom = paddingValue) } } else { PaddingValues(all = 0.dp) @@ -391,18 +385,18 @@ private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues } @Stable -private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): Shape { +private fun getItemShape(roundingMode: RoundingModeUM, radius: Dp): Shape { return when (roundingMode) { - is DraggableItem.RoundingMode.None -> RectangleShape - is DraggableItem.RoundingMode.Top -> RoundedCornerShape( + is RoundingModeUM.None -> RectangleShape + is RoundingModeUM.Top -> RoundedCornerShape( topStart = radius, topEnd = radius, ) - is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape( + is RoundingModeUM.Bottom -> RoundedCornerShape( bottomStart = radius, bottomEnd = radius, ) - is DraggableItem.RoundingMode.All -> RoundedCornerShape( + is RoundingModeUM.All -> RoundedCornerShape( size = radius, ) } @@ -423,8 +417,8 @@ private fun OrganizeTokensScreenPreview( private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.organizeTokensState, - WalletPreviewData.groupedOrganizeTokensState, + OrganizeTokensPreviewLegacy.stateAccounts, + OrganizeTokensPreviewLegacy.state, ), ) // endregion Preview \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreviewLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreviewLegacy.kt new file mode 100644 index 0000000000..caf4239496 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreviewLegacy.kt @@ -0,0 +1,127 @@ +package com.tangem.feature.wallet.child.organizetokens.ui.preview + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import java.util.UUID + +internal object OrganizeTokensPreviewLegacy { + + private const val networksSize = 10 + private const val tokensSize = 3 + + private val tokenItemDragState by lazy { + TokenItemState.Draggable( + id = UUID.randomUUID().toString(), + iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), + ) + } + + private val draggableItems: PersistentList by lazy { + List(networksSize) { it } + .flatMap { index -> + val lastNetworkIndex = networksSize - 1 + val lastTokenIndex = tokensSize - 1 + val networkNumber = index + 1 + + val group = DraggableItem.GroupHeader( + id = networkNumber, + networkName = "$networkNumber", + roundingModeUM = when (index) { + 0 -> RoundingModeUM.Top() + lastNetworkIndex -> RoundingModeUM.Bottom() + else -> RoundingModeUM.None + }, + accountId = "account_$networkNumber", + ) + + val tokens: MutableList = mutableListOf() + repeat(times = tokensSize) { i -> + val tokenNumber = i + 1 + tokens.add( + DraggableItem.Token( + tokenItemState = tokenItemDragState.copy( + id = "${group.id}_token_$tokenNumber", + titleState = TokenItemState.TitleState.Content( + text = stringReference(value = "Token $tokenNumber from $networkNumber network"), + ), + ), + groupId = group.id, + accountId = "account_$networkNumber", + roundingModeUM = when { + i == lastTokenIndex && index == lastNetworkIndex -> RoundingModeUM.Bottom() + else -> RoundingModeUM.None + }, + ), + ) + } + + val divider = DraggableItem.Placeholder( + id = "divider_$networkNumber", + accountId = "account_$networkNumber", + ) + + buildList { + add(group) + addAll(tokens) + if (index != lastNetworkIndex) { + add(divider) + } + } + } + .toPersistentList() + } + + val stateAccounts by lazy { + OrganizeTokensState( + onBackClick = {}, + tokenListUM = OrganizeTokensListUM.AccountList( + items = draggableItems, + isGrouped = true, + ), + header = OrganizeTokensState.HeaderConfig( + onSortClick = {}, + onGroupClick = {}, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = { _, _ -> }, + onItemDragStart = {}, + canDragItemOver = { _, _ -> false }, + onItemDragEnd = {}, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = {}, + onCancelClick = {}, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + val state by lazy { + stateAccounts.copy( + tokenListUM = OrganizeTokensListUM.TokensList( + items = draggableItems, + isGrouped = true, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt new file mode 100644 index 0000000000..aa0ae8e9d7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.child.tokenActions + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.getDefaultRowColors +import com.tangem.core.ui.components.getWarningRowColors +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class TokenActionsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + TangemBottomSheet( + containerColor = TangemTheme.colors.background.primary, + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + ) { + Column { + params.actions.fastForEach { action -> + if (action.isEnabled) { + val rowColors = if (action.isWarning) { + getWarningRowColors() + } else { + getDefaultRowColors() + } + SimpleSettingsRow( + title = action.text.resolveReference(), + icon = action.iconResId, + enabled = action.isEnabled, + rowColors = rowColors, + onItemsClick = action.onClick, + ) + } + } + } + } + } + + data class Params( + val actions: List, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 8d96777c9e..f7c026a60f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss @@ -14,21 +15,22 @@ 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.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2 import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle -import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -38,18 +40,18 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@OptIn(ExperimentalDecomposeApi::class) @Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, - marketsEntryComponentFactory: MarketsEntryComponent.Factory, feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, + private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -60,9 +62,6 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } - private val marketsEntryComponent by lazy { - marketsEntryComponentFactory.create(child("marketsEntryComponent")) - } init { lifecycle.subscribe(model.screenLifecycleProvider) @@ -138,6 +137,15 @@ internal class WalletComponent @AssistedInject constructor( ), ) } + is WalletDialogConfig.TokenActionList -> { + TokenActionsComponent( + appComponentContext = childByContext(componentContext), + params = TokenActionsComponent.Params( + actions = dialogConfig.actionList, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) @@ -148,18 +156,33 @@ internal class WalletComponent @AssistedInject constructor( var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() - WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, - bottomSheetContent = { - BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = modifier, - ) - }, - bottomSheetHeaderHeightProvider = { headerSize }, - onBottomSheetStateChange = { bottomSheetState.value = it }, - ) + if (designFeatureToggles.isRedesignEnabled) { + WalletScreen2( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } else { + WalletScreen( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } when (val dialog = dialog.child?.instance) { is ComposableDialogComponent -> dialog.Dialog() @@ -174,19 +197,11 @@ internal class WalletComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier, ) { - if (feedFeatureToggle.isFeedEnabled) { - feedEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } else { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) } @AssistedFactory 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 256d99fb51..46a932bf55 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 @@ -11,9 +11,10 @@ import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -26,12 +27,16 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.* +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase 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.WalletDialogConfig @@ -43,11 +48,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedCallbacks 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.pushnotifications.api.PushNotificationsModelCallbacks -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.coroutines.* @@ -80,7 +82,6 @@ internal class WalletModel @Inject constructor( private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val walletImageResolver: WalletImageResolver, - private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, private val walletContentFetcher: WalletContentFetcher, @@ -90,19 +91,17 @@ internal class WalletModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val userWalletsListRepository: UserWalletsListRepository, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, - private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, private val trackingContextProxy: TrackingContextProxy, private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val feedFeatureToggle: FeedFeatureToggle, private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, private val appsFlyerStore: AppsFlyerStore, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getWalletIconUseCase: GetWalletIconUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -116,13 +115,12 @@ internal class WalletModel @Inject constructor( private val refreshWalletJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() - private var needToRefreshWallet = false - private var expressTxStatusTaskScheduler = SingleTaskScheduler() + private var shouldRefreshWallet = false + private val expressTxStatusTaskScheduler = SingleTaskScheduler() init { trackScreenOpened() - updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -159,17 +157,9 @@ internal class WalletModel @Inject constructor( } } - private fun updateMarketToggle() { - stateHolder.update { - it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) - } - } - private fun updateYieldSupplyApy() { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { - modelScope.launch(dispatchers.default) { - yieldSupplyApyUpdateUseCase() - } + modelScope.launch(dispatchers.default) { + yieldSupplyApyUpdateUseCase() } } @@ -188,7 +178,6 @@ internal class WalletModel @Inject constructor( override fun onDestroy() { super.onDestroy() - tokenListStore.clear() stateHolder.clear() walletScreenContentLoader.cancelAll() } @@ -235,6 +224,7 @@ internal class WalletModel @Inject constructor( } val result = getAppThemeModeUseCase().firstOrNull() val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code analyticsEventsHandler.send( WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( hasMobileWallet = hasMobileWallet, @@ -242,6 +232,7 @@ internal class WalletModel @Inject constructor( theme = theme.value, isImported = selectedWallet.isImported(), referralId = appsFlyerStore.get()?.refcode, + appCurrency = appCurrency, ), ) } @@ -267,9 +258,9 @@ internal class WalletModel @Inject constructor( getWalletsUseCase() .conflate() .distinctUntilChanged() - .map { + .map { userWallets -> walletsUpdateActionResolver.resolve( - wallets = it, + wallets = userWallets, currentState = stateHolder.value, ) } @@ -361,7 +352,7 @@ internal class WalletModel @Inject constructor( refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> { + shouldRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() } } @@ -394,7 +385,6 @@ internal class WalletModel @Inject constructor( * Update state each time a user opens/returns to wallet screen * and every minute while user stays on the main screen */ - if (!tangemPayFeatureToggles.isTangemPayEnabled) return combine( flow = screenLifecycleProvider.isBackgroundState, @@ -433,12 +423,12 @@ internal class WalletModel @Inject constructor( private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) - needToRefreshWallet = true + shouldRefreshWallet = true }.saveIn(refreshWalletJobHolder) } private fun triggerRefreshWalletQuotes() { - needToRefreshWallet = false + shouldRefreshWallet = false val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return modelScope.launch { @@ -469,7 +459,6 @@ internal class WalletModel @Inject constructor( // refresh loader to use actual user wallet walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, isRefresh = true, coroutineScope = modelScope, ) @@ -517,10 +506,9 @@ internal class WalletModel @Inject constructor( } private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { - action.wallets.forEach { + action.wallets.forEach { userWallet -> walletScreenContentLoader.load( - userWallet = it, - clickIntents = clickIntents, + userWallet = userWallet, coroutineScope = modelScope, isRefresh = true, ) @@ -534,12 +522,12 @@ internal class WalletModel @Inject constructor( wallets = action.wallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -566,11 +554,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) { walletScreenContentLoader.cancel(action.prevWalletId) - tokenListStore.remove(action.prevWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -582,6 +568,7 @@ internal class WalletModel @Inject constructor( newUserWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) } @@ -589,11 +576,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) { action.wallets.forEach { userWallet -> walletScreenContentLoader.cancel(userWallet.walletId) - tokenListStore.remove(userWallet.walletId) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -604,45 +589,28 @@ internal class WalletModel @Inject constructor( userWallet = userWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) } } private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { - if (accountsFeatureToggles.isFeatureEnabled) { - fetchWalletContent(userWallet = action.selectedWallet) + fetchWalletContent(userWallet = action.selectedWallet) - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - - walletScreenContentLoader.load( + stateHolder.update( + AddWalletTransformer( userWallet = action.selectedWallet, clickIntents = clickIntents, - coroutineScope = modelScope, - ) - } else { - walletScreenContentLoader.load( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - coroutineScope = modelScope, - ) + walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, + ), + ) - fetchWalletContent(userWallet = action.selectedWallet) - - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - } + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + coroutineScope = modelScope, + ) scrollToWallet(prevIndex = action.prevWalletIndex, newIndex = action.selectedWalletIndex) { stateHolder.update { @@ -655,11 +623,9 @@ internal class WalletModel @Inject constructor( private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) - tokenListStore.remove(action.deletedWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -698,12 +664,12 @@ internal class WalletModel @Inject constructor( unlockedWallets = action.unlockedWallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) 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 1f63082dad..fccfccfd12 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 @@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType import timber.log.Timber import javax.inject.Inject @@ -110,7 +111,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( when (walletState) { is WalletState.MultiCurrency -> { val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } - walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + walletState.type == WalletType.Hot && wallet is UserWallet.Cold } else -> false } @@ -212,7 +213,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId } ?: return@filter false wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency && - previousState.type == WalletState.MultiCurrency.WalletType.Hot + previousState.type == WalletType.Hot } return Action.ReinitializeWallets(selectedWallet, walletsToUpdate) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1638e3ed00..debd9cec80 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -2,6 +2,9 @@ package com.tangem.feature.wallet.child.wallet.model.intents import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R @@ -22,12 +25,12 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer -import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -64,7 +67,6 @@ internal interface TangemPayIntents { @ModelScoped internal class TangemPayClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, - private val featureToggles: TangemPayFeatureToggles, private val onboardingRepository: OnboardingRepository, private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase, private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase, @@ -73,13 +75,12 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val tangemPayOnboardingRepository: OnboardingRepository, private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), TangemPayIntents { override suspend fun onPullToRefresh() { val userWalletId = stateHolder.getSelectedWalletId() - if (!featureToggles.isTangemPayEnabled || - !onboardingRepository.isTangemPayInitialDataProduced(userWalletId) - ) { + if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) @@ -173,6 +174,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private fun goToSupportForRejectKyc(customerId: String) { modelScope.launch { + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay)) sendFeedbackEmailUseCase( type = FeedbackEmailType.Visa.KycRejected( walletMetaInfo = WalletMetaInfo( @@ -231,6 +233,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( userWalletId = stateHolder.getSelectedWalletId(), ).getOrNull() ?: return@launch + analyticsEventHandler.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay)) sendFeedbackEmailUseCase( FeedbackEmailType.Visa.FailedIssueCard( walletMetaInfo = cardInfo, @@ -242,6 +245,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( override fun onOnboardingBannerClick(userWalletId: UserWalletId) { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.MainVisaPermanentBannerClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain) @@ -259,6 +263,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( } private fun disableTangemPay(userWalletId: UserWalletId) { + analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled()) modelScope.launch { tangemPayOnboardingRepository.disableTangemPay(userWalletId) .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 41161fa227..8f03d6ae84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -30,7 +30,7 @@ internal class WalletClickIntents @Inject constructor( private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor, - private val stateHolder: WalletStateController, + private val stateController: WalletStateController, private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, @@ -62,7 +62,7 @@ internal class WalletClickIntents @Inject constructor( fun onWalletChange(index: Int, onlyState: Boolean) { if (onlyState) { - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } return } @@ -70,27 +70,23 @@ internal class WalletClickIntents @Inject constructor( launch { neverToShowWalletsScrollPreview() } val maybeUserWallet = selectWalletUseCase( - userWalletId = stateHolder.value.wallets[index].walletCardState.id, + userWalletId = stateController.value.wallets[index].walletCardState.id, ) - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } - maybeUserWallet.onRight { - if (!it.isLocked) { - launch { walletContentFetcher(userWalletId = it.walletId) } + maybeUserWallet.onRight { userWallet -> + if (!userWallet.isLocked) { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } } - walletScreenContentLoader.load( - userWallet = it, - clickIntents = this@WalletClickIntents, - coroutineScope = modelScope, - ) + walletScreenContentLoader.load(userWallet = userWallet, coroutineScope = modelScope) } } } fun onRefreshSwipe(showRefreshState: Boolean) { - when (stateHolder.getSelectedWallet()) { + when (stateController.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { refreshMultiCurrencyContent(showRefreshState) } @@ -111,7 +107,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -126,7 +122,7 @@ internal class WalletClickIntents @Inject constructor( } .awaitAll() - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } @@ -137,7 +133,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -147,12 +143,11 @@ internal class WalletClickIntents @Inject constructor( onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = this@WalletClickIntents, isRefresh = true, coroutineScope = modelScope, ) - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } 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 f4769cd5bf..8b1fd04f57 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 @@ -9,6 +9,7 @@ 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.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance @@ -18,7 +19,6 @@ import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -28,7 +28,10 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory 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.* +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.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter @@ -48,16 +51,11 @@ internal interface WalletContentClickIntents { fun onDismissMarketsTooltip() - fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) + fun onTokenItemClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus) - fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onApyLabelClick( - userWalletId: UserWalletId, - currencyStatus: CryptoCurrencyStatus, - apySource: ApySource, - apy: String, - ) + fun onApyLabelClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus, apySource: ApySource, apy: String) fun onYieldPromoCloseClick() @@ -69,6 +67,8 @@ internal interface WalletContentClickIntents { fun onAccountCollapseClick(account: Account) + fun onManageTokensClick(accountId: AccountId) + fun onTransactionClick(txHash: String) fun onDissmissBottomSheet() @@ -119,12 +119,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { - router.openTokenDetails(userWalletId, currencyStatus) + override fun onTokenItemClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus) { + router.openTokenDetails(accountId.userWalletId, currencyStatus) } - override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { + val userWalletId = accountId.userWalletId val userWallet = getUserWalletUseCase(userWalletId).getOrElse { Timber.e( """ @@ -139,14 +140,21 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) .take(count = 1) - .collectLatest { - showActionsBottomSheet(it, userWallet) + .collectLatest { actionsState -> + router.openTokenActionSheet( + userWallet = userWallet, + tokenActionList = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + accountId = accountId, + clickIntents = currencyActionsClickIntents, + ).convert(actionsState), + ) } } } override fun onApyLabelClick( - userWalletId: UserWalletId, + accountId: AccountId, currencyStatus: CryptoCurrencyStatus, apySource: ApySource, apy: String, @@ -161,9 +169,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( sendApyLabelClickAnalytics(navigationAction, currencyStatus) when (navigationAction) { - is NavigationAction.Staking -> router.openTokenDetails(userWalletId, currencyStatus, navigationAction) + is NavigationAction.Staking -> router.openTokenDetails( + accountId.userWalletId, + currencyStatus, + navigationAction, + ) is NavigationAction.YieldSupply -> openYieldSupply( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, cryptoCurrencyStatus = currencyStatus, apy = apy, ) @@ -201,6 +213,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onAccountExpandClick(account: Account) { analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens()) accountDependencies.expandedAccountsHolder.expandAccount(account.accountId) + walletEventSender.send(WalletEvent.CollapseBalance) } override fun onAccountCollapseClick(account: Account) { @@ -208,6 +221,10 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId) } + override fun onManageTokensClick(accountId: AccountId) { + router.openManageTokensScreen(accountId) + } + private fun openYieldSupply(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, apy: String) { router.openYieldSupplyEntryScreen( userWalletId = userWalletId, @@ -247,18 +264,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(event) } - private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { - stateHolder.showBottomSheet( - ActionsBottomSheetConfig( - actions = MultiWalletCurrencyActionsConverter( - userWallet = userWallet, - clickIntents = currencyActionsClickIntents, - ).convert(tokenActionsState), - ), - userWallet.walletId, - ) - } - override fun onTransactionClick(txHash: String) { modelScope.launch(dispatchers.main) { val currency = getSingleCryptoCurrencyStatusUseCase.unwrap( 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 e6baa06b02..3635996d11 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 @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -11,8 +12,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.analytics.models.event.OfframpAnalyticsEvent 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.clipboard.ClipboardManager import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference @@ -20,8 +23,6 @@ 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 import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap @@ -31,18 +32,18 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent @@ -63,20 +64,18 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject interface WalletCurrencyActionsClickIntents { fun onSendClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) @@ -84,32 +83,28 @@ interface WalletCurrencyActionsClickIntents { fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) fun onBuyClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - userWalletId: UserWalletId, + accountId: AccountId, unavailabilityReason: ScenarioUnavailabilityReason, ) - fun onReceiveClick( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - event: AnalyticsEvent? = null, - ) + fun onReceiveClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null) - fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?) + fun onStakeClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? - fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onHideTokensClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onPerformHideToken(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onExploreClick() @@ -136,26 +131,23 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -179,18 +171,18 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSend(cryptoCurrencyStatus, userWalletId) + navigateToSend(cryptoCurrencyStatus, accountId.userWalletId) } }, ) } else { - navigateToSend(cryptoCurrencyStatus, userWalletId) + navigateToSend(cryptoCurrencyStatus, accountId.userWalletId) } } } override fun onReceiveClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?, ) { @@ -243,7 +235,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return resourceReference(R.string.wallet_notification_address_copied) } - override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onCopyAddressClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( token = cryptoCurrencyStatus.currency.symbol, @@ -254,10 +246,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { walletManagersFacade.getDefaultAddress( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, network = cryptoCurrencyStatus.currency.network, )?.let { address -> - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) clipboardManager.setText(text = address, isSensitive = true) walletEventSender.send(event = WalletEvent.CopyAddress) @@ -265,7 +257,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onHideTokensClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), ) @@ -273,39 +265,23 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val currency = cryptoCurrencyStatus.currency val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && - !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) + !isCryptoCurrencyCoinCouldHide(userWalletId = accountId.userWalletId, cryptoCurrencyCoin = currency) if (isCryptoCurrencyCoinCouldHide) { uiMessageSender.send(WalletAlertUM.unableHideToken(cryptoCurrency = cryptoCurrencyStatus.currency)) } else { uiMessageSender.send( WalletAlertUM.hideTokenConfirm(cryptoCurrency = cryptoCurrencyStatus.currency) { - onPerformHideToken(userWalletId, cryptoCurrencyStatus) + onPerformHideToken(accountId, cryptoCurrencyStatus) }, ) } } } - override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onPerformHideToken(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - ) - .map { it.account.accountId } - .getOrNull() - - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") - return@launch - } - - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) - } else { - removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) - } + manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) .fold( ifLeft = { walletEventSender.send( @@ -313,7 +289,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + router.dialogNavigation.dismiss() }, ) } @@ -335,18 +311,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { modelScope.launch(dispatchers.main) { - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyStatus, - appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } override fun onBuyClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -362,7 +339,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Onramp( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, currency = cryptoCurrencyStatus.currency, source = OnrampSource.TOKEN_LONG_TAP, ), @@ -371,7 +348,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - userWalletId: UserWalletId, + accountId: AccountId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -394,12 +371,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSwap(cryptoCurrencyStatus, userWalletId) + navigateToSwap(cryptoCurrencyStatus, accountId.userWalletId) } }, ) } else { - navigateToSwap(cryptoCurrencyStatus, userWalletId) + navigateToSwap(cryptoCurrencyStatus, accountId.userWalletId) } } } @@ -441,11 +418,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onStakeClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?, ) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) val integrationId = option?.integrationId ?: return @@ -454,7 +431,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Staking( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, cryptoCurrency = cryptoCurrency, integrationId = integrationId, ), @@ -472,9 +449,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return - val tokenListState = selectedWallet.tokensListState - - when (tokenListState) { + when (val tokenListState = selectedWallet.tokensListState) { is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability( tokenCount = tokenListState.items.count { it is TokensListItemUM.Token }, ) @@ -663,8 +638,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } private suspend fun needShowYieldSupplyWarning(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { 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 0cdab04aa3..c3b9d8d1f2 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 @@ -8,6 +8,7 @@ import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.userwallet.handle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.ButtonSupport import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener @@ -234,6 +235,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( val userWallet = getSelectedUserWallet() ?: return@launch val cardInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main)) sendFeedbackEmailUseCase(type = FeedbackEmailType.RateCanBeBetter(walletMetaInfo = cardInfo)) } } @@ -336,6 +338,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main)) sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(walletMetaInfo = metaInfo)) } } @@ -345,6 +348,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main)) sendFeedbackEmailUseCase(type = FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt index 913e1262d2..751beab4d4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.di import com.tangem.core.decompose.model.Model import com.tangem.feature.wallet.DefaultWalletEntryComponent -import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedModel import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher @@ -41,9 +40,4 @@ internal interface WalletFeatureModule { @IntoMap @ClassKey(KycRejectedModel::class) fun bindKycRejectedModel(model: KycRejectedModel): Model - - @Binds - @IntoMap - @ClassKey(OrganizeTokensModel::class) - fun bindOrganizeTokensModel(model: OrganizeTokensModel): Model } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index 4119478f5a..7303bfbb6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.account import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -9,7 +8,6 @@ import javax.inject.Inject @ModelScoped internal class AccountDependencies @Inject constructor( - val accountsFeatureToggles: AccountsFeatureToggles, val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, val expandedAccountsHolder: ExpandedAccountsHolder, val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt deleted file mode 100644 index ba99940e19..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.tangem.feature.wallet.presentation.common - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.event.consumedEvent -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.wallet.UserWalletId -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.wallet.state.model.* -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import java.util.UUID - -@Suppress("LargeClass") -internal object WalletPreviewData { - - val topBarConfig by lazy { WalletTopBarConfig(onDetailsClick = {}) } - - val walletCardContentState by lazy { - WalletCardState.Content( - id = UserWalletId(stringValue = "123"), - title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1", - balance = "8923,05312312312312312312331231231233432423423424234 $", - additionalInfo = WalletAdditionalInfo( - hideable = false, - content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), - ), - imageResId = R.drawable.ill_wallet2_cards3_120_106, - dropDownItems = persistentListOf(), - cardCount = 1, - isZeroBalance = false, - isBalanceFlickering = false, - ) - } - - val walletCardLoadingState by lazy { - WalletCardState.Loading( - id = UserWalletId("321"), - title = "Wallet 1", - imageResId = R.drawable.ill_wallet2_cards3_120_106, - dropDownItems = persistentListOf(), - ) - } - - val walletCardErrorState by lazy { - WalletCardState.Error( - id = UserWalletId("24"), - title = "Wallet 1", - imageResId = R.drawable.ill_wallet2_cards3_120_106, - dropDownItems = persistentListOf(), - ) - } - - val wallets by lazy { - mapOf( - UserWalletId(stringValue = "123") to walletCardContentState, - UserWalletId(stringValue = "321") to walletCardLoadingState, - UserWalletId(stringValue = "24") to walletCardErrorState, - ) - } - - private val tokenItemDragState by lazy { - TokenItemState.Draggable( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), - ) - } - - private const val networksSize = 10 - private const val tokensSize = 3 - private val draggableItems: PersistentList by lazy { - List(networksSize) { it } - .flatMap { index -> - val lastNetworkIndex = networksSize - 1 - val lastTokenIndex = tokensSize - 1 - val networkNumber = index + 1 - - val group = DraggableItem.GroupHeader( - id = networkNumber, - networkName = "$networkNumber", - - roundingMode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() - lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - accountId = "account_$networkNumber", - ) - - val tokens: MutableList = mutableListOf() - repeat(times = tokensSize) { i -> - val tokenNumber = i + 1 - tokens.add( - DraggableItem.Token( - tokenItemState = tokenItemDragState.copy( - id = "${group.id}_token_$tokenNumber", - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Token $tokenNumber from $networkNumber network"), - ), - ), - groupId = group.id, - accountId = "account_$networkNumber", - roundingMode = when { - i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - ), - ) - } - - val divider = DraggableItem.Placeholder( - id = "divider_$networkNumber", - accountId = "account_$networkNumber", - ) - - buildList { - add(group) - addAll(tokens) - if (index != lastNetworkIndex) { - add(divider) - } - } - } - .toPersistentList() - } - - private val draggableTokens by lazy { - draggableItems - .filterIsInstance() - .toMutableList() - .also { - it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top()) - } - .toPersistentList() - } - - val groupedOrganizeTokensState by lazy { - OrganizeTokensState( - onBackClick = {}, - itemsState = OrganizeTokensListState.GroupedByNetwork( - items = draggableItems, - ), - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = {}, - onGroupClick = {}, - ), - dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = { _, _ -> }, - onItemDragStart = {}, - canDragItemOver = { _, _ -> false }, - onItemDragEnd = {}, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = {}, - onCancelClick = {}, - ), - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - val organizeTokensState by lazy { - groupedOrganizeTokensState.copy( - itemsState = OrganizeTokensListState.Ungrouped( - items = draggableTokens, - ), - ) - } - - val bottomSheet by lazy { - TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = {}, - onScanClick = {}, - ), - ) - } - - val actionsBottomSheet = ActionsBottomSheetConfig( - actions = listOf( - TokenActionButtonConfig( - text = TextReference.Str("Send"), - iconResId = R.drawable.ic_share_24, - isWarning = false, - onClick = {}, - ), - ).toImmutableList(), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt new file mode 100644 index 0000000000..626da24cb8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -0,0 +1,58 @@ +package com.tangem.feature.wallet.presentation.common + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import kotlinx.collections.immutable.persistentListOf + +@Suppress("LargeClass") +internal object WalletPreviewDataLegacy { + + val topBarConfig by lazy { WalletTopBarConfig(onDetailsClick = {}) } + + val walletCardContentState by lazy { + WalletCardState.Content( + id = UserWalletId(stringValue = "123"), + title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1", + balance = "8923,05312312312312312312331231231233432423423424234 $", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + ), + imageResId = R.drawable.ill_wallet2_cards3_120_106, + dropDownItems = persistentListOf(), + cardCount = 1, + isZeroBalance = false, + isBalanceFlickering = false, + ) + } + + val walletCardLoadingState by lazy { + WalletCardState.Loading( + id = UserWalletId("321"), + title = "Wallet 1", + imageResId = R.drawable.ill_wallet2_cards3_120_106, + dropDownItems = persistentListOf(), + ) + } + + val walletCardErrorState by lazy { + WalletCardState.Error( + id = UserWalletId("24"), + title = "Wallet 1", + imageResId = R.drawable.ill_wallet2_cards3_120_106, + dropDownItems = persistentListOf(), + ) + } + + val wallets by lazy { + mapOf( + UserWalletId(stringValue = "123") to walletCardContentState, + UserWalletId(stringValue = "321") to walletCardLoadingState, + UserWalletId(stringValue = "24") to walletCardErrorState, + ) + } +} \ 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 59f9ca6314..2b44fa4727 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 @@ -2,243 +2,98 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState -import com.tangem.core.ui.components.token.AccountItemPreviewData -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig +import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview +import com.tangem.feature.wallet.presentation.preview.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList internal object WalletScreenPreviewData { - private val tokenItemState = TokenItemState.Content( + + private val tokenRowDefault = TangemTokenRowUM.Content( id = "1", - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "34 496,75 \$", - priceChangePercent = "0,43 %", - type = PriceChangeType.DOWN, + headIconUM = TangemIconUM.Currency(CurrencyIconState.Loading), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference("Bitcoin"), ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = stringReference("Bitcoin"), + ), + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("1 234,56 \$"), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("0,12345678 BTC"), + ), + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemRowTailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) - private val textContentTokensState = WalletTokensListState.ContentState.Content( - items = persistentListOf( - TokensListItemUM.GroupTitle(id = 1, text = stringReference("Network Bitcoin")), - TokensListItemUM.Token(state = tokenItemState), - TokensListItemUM.GroupTitle(id = 2, text = stringReference("Network Ethereum")), - TokensListItemUM.Token( - state = tokenItemState.copy( - id = "2", - titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "1 799,41 \$", - priceChangePercent = "5,16 %", - type = PriceChangeType.UP, - ), - ), - ), - TokensListItemUM.Token( - state = TokenItemState.Unreachable( - id = "3", - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - onItemClick = {}, - onItemLongClick = {}, - ), - ), - TokensListItemUM.Token( - state = tokenItemState.copy( - id = "4", - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "0.01 \$", - priceChangePercent = "1,34 %", - type = PriceChangeType.DOWN, - ), - ), - ), + private val tokenListDefault = WalletTokensListUM.Content( + tokenList = persistentListOf( + TokensListItemUM2.Token(tokenRowDefault.copy(id = "0")), + TokensListItemUM2.Token(tokenRowDefault.copy(id = "1")), + TokensListItemUM2.Token(tokenRowDefault.copy(id = "2")), ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, + organizeButtonUM = TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + type = TangemButtonType.Secondary, onClick = {}, ), ) - private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( - items = persistentListOf( - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = false, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem - .copy(iconState = AccountItemPreviewData.accountLetterIcon), - ), - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = true, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem, - ), - ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, - onClick = {}, - ), + private val walletLocked = WalletUM.Locked( + walletsBalanceUM = WalletBalancePreview.content, + buttons = WalletPreviewData.actionButtons, + type = WalletType.Cold, + notifications = persistentListOf(), ) - private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent( - items = persistentListOf( - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = false, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem - .copy(iconState = AccountItemPreviewData.accountLetterIcon), - ), - TokensListItemUM.Portfolio( - tokens = persistentListOf(), - isExpanded = true, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem, - ), + private val walletDefault = WalletUM.Content( + walletsBalanceUM = WalletBalancePreview.content, + buttons = WalletPreviewData.actionButtons, + type = WalletType.Cold, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, - onClick = {}, + notifications = persistentListOf(), + notificationsCarousel = persistentListOf(), + tokensListUM = tokenListDefault, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(), + collectionsCount = 0, + allAssetsCount = 0, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = {}, ), + tangemPayState = TangemPayState.Loading, ) - private val noteLockedCard by lazy { - WalletCardState.LockedContent( - id = UserWalletId(stringValue = "1"), - title = "Note", - additionalInfo = WalletAdditionalInfo( - hideable = false, - content = TextReference.Str("Locked"), - ), - imageResId = R.drawable.ill_note_btc_120_106, - dropDownItems = persistentListOf(), - ) - } - private val miltiUnreachableCard by lazy { - WalletCardState.Content( - id = UserWalletId(stringValue = "2"), - title = "Wallet 1", - additionalInfo = WalletAdditionalInfo( - hideable = false, - content = TextReference.Str("Seed phrase"), - ), - imageResId = R.drawable.ill_wallet2_cards3_120_106, - cardCount = 3, - balance = DASH_SIGN, - dropDownItems = persistentListOf(), - isZeroBalance = false, - isBalanceFlickering = false, - ) - } - private val multiWalletState by lazy { - WalletState.MultiCurrency.Content( - pullToRefreshConfig = PullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - walletCardState = miltiUnreachableCard, - buttons = persistentListOf(buyButton), - warnings = persistentListOf( - WalletNotification.Warning.SomeNetworksUnreachable, - WalletNotification.FinishWalletActivation( - type = WalletActivationBannerType.Attention, - buttonsState = ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.hw_activation_need_finish), - onClick = { }, - ), - isBackupExists = false, - ), - ), - bottomSheetConfig = null, - tokensListState = textContentTokensState, - nftState = WalletNFTItemUM.Content( - previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), - collectionsCount = 1, - allAssetsCount = 3, - noCollectionAssetsCount = 0, - isFlickering = false, - onItemClick = { }, - ), - tangemPayState = TangemPayState.Empty, - type = WalletState.MultiCurrency.WalletType.Cold, - ) - } - - private val buyButton = WalletManageButton.Buy(enabled = false, dimContent = true, onClick = {}) - private val sendButton = WalletManageButton.Send(enabled = false, dimContent = true, onClick = {}) - private val receiveButton = WalletManageButton.Receive( - enabled = false, - dimContent = true, - onClick = {}, - onLongClick = null, - ) - - private val singleWalletLockedState = WalletState.SingleCurrency.Locked( - walletCardState = noteLockedCard, - buttons = persistentListOf( - buyButton, - sendButton, - receiveButton, - ), - bottomSheetConfig = null, - onUnlockNotificationClick = {}, - onExploreClick = {}, - ) - - internal val walletScreenState = WalletScreenState( + internal val defaultState = WalletScreenState( topBarConfig = topBarConfig, selectedWalletIndex = 0, - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState, + wallets = persistentListOf(), + wallets2 = persistentListOf( + walletLocked, + walletDefault, ), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) - - internal val accountScreenState = - walletScreenState.copy( - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState.copy(tokensListState = portfolioContentState), - ), - ) - - internal val accountScreenWithEmptyTokensState = - walletScreenState.copy( - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState.copy(tokensListState = emptyPortfolioContentState), - ), - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt new file mode 100644 index 0000000000..5485587450 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -0,0 +1,265 @@ +package com.tangem.feature.wallet.presentation.common.preview + +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.token.AccountItemPreviewData +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.utils.StringsSigns.DASH_SIGN +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal object WalletScreenPreviewDataLegacy { + + private val buyButton = WalletManageButton.Buy(enabled = false, dimContent = true, onClick = {}) + private val sendButton = WalletManageButton.Send(enabled = false, dimContent = true, onClick = {}) + private val receiveButton = WalletManageButton.Receive( + enabled = false, + dimContent = true, + onClick = {}, + onLongClick = null, + ) + + private val tokenItemState = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ) + + private val textContentTokensState = WalletTokensListState.ContentState.Content( + items = persistentListOf( + TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")), + TokensListItemUM.Token(state = tokenItemState), + TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "2", + titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "1 799,41 \$", + priceChangePercent = "5,16 %", + type = PriceChangeType.UP, + ), + ), + ), + TokensListItemUM.Token( + state = TokenItemState.Unreachable( + id = "3", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + onItemClick = {}, + onItemLongClick = {}, + ), + ), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "4", + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "0.01 \$", + priceChangePercent = "1,34 %", + type = PriceChangeType.DOWN, + ), + ), + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( + items = persistentListOf( + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent( + items = persistentListOf( + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Empty( + action = PortfolioItemContentUM.Empty.Action( + text = resourceReference(id = R.string.onboarding_add_tokens), + onClick = {}, + ), + ), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val noteLockedCard by lazy { + WalletCardState.LockedContent( + id = UserWalletId(stringValue = "1"), + title = "Note", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Locked"), + ), + imageResId = R.drawable.ill_note_btc_120_106, + dropDownItems = persistentListOf(), + ) + } + private val miltiUnreachableCard by lazy { + WalletCardState.Content( + id = UserWalletId(stringValue = "2"), + title = "Wallet 1", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Seed phrase"), + ), + imageResId = R.drawable.ill_wallet2_cards3_120_106, + cardCount = 3, + balance = DASH_SIGN, + dropDownItems = persistentListOf(), + isZeroBalance = false, + isBalanceFlickering = false, + ) + } + private val multiWalletState by lazy { + WalletState.MultiCurrency.Content( + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + walletCardState = miltiUnreachableCard, + buttons = persistentListOf(buyButton), + warnings = persistentListOf( + WalletNotification.Warning.SomeNetworksUnreachable, + WalletNotification.FinishWalletActivation( + type = WalletActivationBannerType.Attention, + buttonsState = ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = { }, + ), + isBackupExists = false, + ), + ), + bottomSheetConfig = null, + tokensListState = textContentTokensState, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), + collectionsCount = 1, + allAssetsCount = 3, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + tangemPayState = TangemPayState.Card( + lastFourDigits = stringReference("*1234"), + balanceText = stringReference("$10"), + balanceSymbol = stringReference("USDC"), + onClick = {}, + ), + type = WalletType.Cold, + ) + } + + private val singleWalletLockedState = WalletState.SingleCurrency.Locked( + walletCardState = noteLockedCard, + buttons = persistentListOf( + buyButton, + sendButton, + receiveButton, + ), + bottomSheetConfig = null, + onUnlockNotificationClick = {}, + onExploreClick = {}, + ) + + internal val walletScreenState = WalletScreenState( + topBarConfig = topBarConfig, + selectedWalletIndex = 0, + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState, + ), + wallets2 = persistentListOf(), + onWalletChange = { _, _ -> }, + event = consumedEvent(), + isHidingMode = false, + showMarketsOnboarding = false, + onDismissMarketsTooltip = {}, + ) + + internal val accountScreenState = + walletScreenState.copy( + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState.copy(tokensListState = portfolioContentState), + ), + ) + + internal val accountScreenWithEmptyTokensState = + walletScreenState.copy( + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState.copy(tokensListState = emptyPortfolioContentState), + ), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt deleted file mode 100644 index f1eb7915cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens - -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverterV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -internal class OrganizeTokensStateHolder( - private val intents: OrganizeTokensIntents, - private val dragAndDropIntents: DragAndDropIntents, - private val dragAndDropAdapterV2: DragAndDropAdapterV2, - private val appCurrencyProvider: Provider, - private val accountsFeatureToggles: AccountsFeatureToggles, -) { - - private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) - - private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) - val itemsConverter = TokenListToListStateConverter( - tokensConverter = tokensConverter, - groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), - ) - - TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) - } - - private val inProgressStateConverter by lazy { InProgressStateConverter() } - - private val tokenListErrorConverter by lazy { - TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - private val tokenListSortingErrorConverter by lazy { - TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - val stateFlow: StateFlow = stateFlowInternal - - fun updateStateWithTokenList(tokenList: TokenList) { - updateState { tokenListConverter.convert(tokenList) } - } - - fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this) - } - } - - fun updateStateAfterTokenListSorting(tokenList: TokenList) { - updateState { - tokenListConverter.convert(tokenList).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateAfterTokenListSortingV2(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateToDisplayProgress() { - updateState { inProgressStateConverter.convert(value = this) } - } - - fun updateStateToHideProgress() { - updateState { inProgressStateConverter.convertBack(value = this) } - } - - fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) { - updateState { copy(tokenListUM = tokenListUM) } - } - - fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { - updateState { copy(itemsState = itemsState) } - } - - fun disableSortingByBalance() { - updateState { copy(header = header.copy(isSortedByBalance = false)) } - } - - fun updateHiddenState(isBalanceHidden: Boolean) { - updateState { copy(isBalanceHidden = isBalanceHidden) } - } - - fun updateStateWithError(error: TokenListError) { - updateState { tokenListErrorConverter.convert(error) } - } - - fun updateStateWithError(error: TokenListSortingError) { - updateState { tokenListSortingErrorConverter.convert(error) } - } - - private fun getInitialState(): OrganizeTokensState { - return OrganizeTokensState( - onBackClick = intents::onBackClick, - itemsState = OrganizeTokensListState.Empty, - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = intents::onSortClick, - onGroupClick = intents::onGroupClick, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = intents::onApplyClick, - onCancelClick = intents::onCancelClick, - ), - dndConfig = if (accountsFeatureToggles.isFeatureEnabled) { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropAdapterV2::onItemDragged, - onItemDragStart = dragAndDropAdapterV2::onItemDraggingStart, - onItemDragEnd = dragAndDropAdapterV2::onItemDraggingEnd, - canDragItemOver = dragAndDropAdapterV2::canDragItemOver, - ) - } else { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropIntents::onItemDragged, - onItemDragStart = dragAndDropIntents::onItemDraggingStart, - onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, - canDragItemOver = dragAndDropIntents::canDragItemOver, - ) - }, - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { - stateFlowInternal.update(block) - } - - private fun consumeScrollListToTopEvent() { - updateState { copy(scrollListToTop = consumedEvent()) } - } -} \ No newline at end of file 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 deleted file mode 100644 index 770ada3985..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ /dev/null @@ -1,60 +0,0 @@ -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.filterCryptoPortfolio -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM - -internal class CryptoCurrenciesIdsResolver { - - fun resolve(listState: OrganizeTokensListState, tokenList: TokenList?): List { - val draggableTokens = when (listState) { - is OrganizeTokensListState.Empty -> return emptyList() - is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() - is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance() - } - val currenciesStatuses = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty, - null, - -> return emptyList() - } - - return draggableTokens.mapNotNull { draggableToken -> - val currencyStatus = currenciesStatuses.firstOrNull { - it.currency.id.value == draggableToken.id - } - - currencyStatus?.currency?.id - } - } - - @Suppress("UseOrEmpty") - fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { - val draggableTokens = when (tokensListUM) { - OrganizeTokensListUM.EmptyList -> return emptyMap() - is OrganizeTokensListUM.AccountList, - is OrganizeTokensListUM.TokensList, - -> tokensListUM.items.filterIsInstance() - } - - return accountStatusList?.accountStatuses - ?.filterCryptoPortfolio() - ?.filter { it.tokenList != TokenList.Empty } - ?.associate { accountStatus -> - val currencies = accountStatus.flattenCurrencies() - accountStatus.account to draggableTokens - .asSequence() - .filter { it.accountId == accountStatus.account.accountId.value } - .mapNotNull { sortedToken -> - currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency - } - .toList() - } ?: emptyMap() - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt deleted file mode 100644 index 80ea9a25b1..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem - -internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder { - return DraggableItem.Placeholder( - id = "placeholder_${accountId}_${index.inc()}", - accountId = accountId, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt deleted file mode 100644 index 2c10ffaf82..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem - -internal fun List.uniteItems(): List { - val items = prepareItems() - val lastItemIndex = items.lastIndex - - return prepareItems().mapIndexed { index, item -> - val mode = when (index) { - // 1 index is used because the first item is always a placeholder, check `prepareItems()` function - 1 -> DraggableItem.RoundingMode.Top() - lastItemIndex -> DraggableItem.RoundingMode.Bottom() - else -> when (item) { - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> DraggableItem.RoundingMode.None - is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) - is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { - DraggableItem.RoundingMode.Bottom(showGap = true) - } else { - DraggableItem.RoundingMode.None - } - } - } - - item - .updateRoundingMode(mode) - .updateShadowVisibility(show = false) - } -} - -internal fun List.uniteItemsV2(isAccountsMode: Boolean): List { - val items = this - val lastItemIndex = items.lastIndex - - return items - .asSequence() - .mapIndexed { index, item -> - val mode = when (index) { - 0 -> if (item is DraggableItem.Placeholder) { - DraggableItem.RoundingMode.None - } else { - DraggableItem.RoundingMode.Top() - } - lastItemIndex -> DraggableItem.RoundingMode.Bottom() - 1 -> if (items.first() is DraggableItem.Placeholder) { - DraggableItem.RoundingMode.Top() - } else { - DraggableItem.RoundingMode.None - } - else -> when (item) { - is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None - is DraggableItem.GroupHeader -> if (isAccountsMode) { - DraggableItem.RoundingMode.None - } else { - DraggableItem.RoundingMode.Top(showGap = true) - } - is DraggableItem.Token -> applyRoundingModeToToken( - isAccountsMode = isAccountsMode, - items = items, - index = index, - lastItemIndex = lastItemIndex, - ) - is DraggableItem.Portfolio -> DraggableItem.RoundingMode.Top(showGap = true) - } - } - - item - .updateRoundingMode(mode) - .updateShadowVisibility(show = false) - }.toList() -} - -internal fun List.divideMovingItem(movingItem: DraggableItem): List { - val mutableList = this.toMutableList() - val listIterator = mutableList.listIterator() - - while (listIterator.hasNext()) { - val item = listIterator.next() - - if (item.id == movingItem.id) { - val dividedItem = movingItem - .updateRoundingMode(DraggableItem.RoundingMode.All()) - .updateShadowVisibility(show = true) - - listIterator.set(dividedItem) - break - } - } - - return mutableList -} - -/** - * !!! Workaround !!! - * - * We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the - * [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item. - * - * @since 07.09.2023 - * */ -private fun List.prepareItems(): List { - val firstPlaceholderId = "initial_placeholder" - val items = this - - return mutableListOf().apply { - add(DraggableItem.Placeholder(firstPlaceholderId)) - - val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId } - - addAll(itemsWithoutFirstPlaceholder) - } -} - -/** - * Applying rounding to tokens - * - * If is in accounts mode without grouping - * * PORTFOLIO - * * TOKEN - * * TOKEN <- add rounding - * * PORTFOLIO index + 1 is PORTFOLIO - * - * If is in accounts mode with grouping - * * PORTFOLIO - * * PLACEHOLDER - * * GROUPING - * * TOKEN - * * TOKEN <- add rounding - * * PLACEHOLDER index + 1 is PLACEHOLDER - * * PORTFOLIO index + 2 is PORTFOLIO - * * PLACEHOLDER - * - * If is not accounts mode without grouping - * * TOKEN - * * TOKEN <- add rounding - * - * If is not accounts mode with grouping - * * PLACEHOLDER - * * GROUPING - * * TOKEN - * * TOKEN <- add rounding - * * PLACEHOLDER index + 1 is PLACEHOLDER - */ -private fun applyRoundingModeToToken( - isAccountsMode: Boolean, - items: List, - index: Int, - lastItemIndex: Int, -) = when { - isAccountsMode && index + 1 < lastItemIndex && - (items[index + 1] is DraggableItem.Portfolio || - items[index + 1] is DraggableItem.Placeholder && items[index + 2] is DraggableItem.Portfolio) -> { - DraggableItem.RoundingMode.Bottom(showGap = true) - } - (!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is DraggableItem.Placeholder -> { - DraggableItem.RoundingMode.Bottom(showGap = true) - } - else -> DraggableItem.RoundingMode.None -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt deleted file mode 100644 index d67847aa0a..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal inline fun OrganizeTokensListState.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListState { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) - is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) - is OrganizeTokensListState.Empty -> this - } -} - -internal inline fun OrganizeTokensListUM.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListUM { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) - is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) - OrganizeTokensListUM.EmptyList -> this - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt deleted file mode 100644 index 8600d8beaf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList - -internal fun TokenList.disableSortingByBalance(): TokenList { - return when (this) { - is TokenList.GroupedByNetwork -> this.copy(sortedBy = TokensSortType.NONE) - is TokenList.Ungrouped -> this.copy(sortedBy = TokensSortType.NONE) - is TokenList.Empty -> this - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt deleted file mode 100644 index d561e3b262..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter - -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenListToStateConverter( - private val currentState: Provider, - private val itemsConverter: TokenListToListStateConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensState { - val state = currentState() - val itemsState = itemsConverter.convert(value) - - return state.copy( - itemsState = itemsState, - header = state.header.copy( - isEnabled = itemsState !is OrganizeTokensListState.Empty, - isSortedByBalance = value.sortedBy == TokensSortType.BALANCE, - isGrouped = value is TokenList.GroupedByNetwork, - ), - actions = state.actions.copy( - canApply = itemsState !is OrganizeTokensListState.Empty, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt deleted file mode 100644 index e2117bfc5f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error - -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenListErrorConverter( - private val currentState: Provider, - private val inProgressStateConverter: InProgressStateConverter, -) : Converter { - - override fun convert(value: TokenListError): OrganizeTokensState { - return inProgressStateConverter.convertBack(currentState()) - } -} \ No newline at end of file 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 deleted file mode 100644 index 79ea585f1d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ /dev/null @@ -1,74 +0,0 @@ -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 -import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants -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.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 -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.orZero -import java.math.BigDecimal - -internal class CryptoCurrencyToDraggableItemConverter( - private val appCurrencyProvider: Provider, -) : Converter { - - private val iconStateConverter = CryptoCurrencyToIconStateConverter() - - override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider()) - } - - override fun convertList(input: Collection): List { - val appCurrency = appCurrencyProvider() - - return input.map { createDraggableToken(it, appCurrency) } - } - - private fun createDraggableToken( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): DraggableItem.Token { - return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.network), - ) - } - - private fun createTokenItemState( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): TokenItemState.Draggable { - val currency = currencyStatus.currency - - return TokenItemState.Draggable( - id = getTokenItemId(currency.id), - iconState = iconStateConverter.convert(currencyStatus), - titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)), - subtitle2State = if (currencyStatus.value.isError) { - TokenItemState.Subtitle2State.Unreachable - } else { - TokenItemState.Subtitle2State.TextContent(text = getFormattedFiatAmount(currencyStatus, appCurrency)) - }, - ) - } - - private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data - val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) - ?.multiply(fiatRate).orZero() - - val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt deleted file mode 100644 index 73ecfe3f79..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -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.getGroupPlaceholder -import com.tangem.utils.converter.Converter - -internal class NetworkGroupToDraggableItemsConverter( - private val itemConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter> { - - override fun convert(value: NetworkGroup): List { - return buildList { - add(createGroupHeader(value)) - addAll(createTokens(value)) - } - } - - override fun convertList(input: Collection): List> { - val lastItemIndex = input.size - 1 - - return input.mapIndexed { index, networkGroup -> - convert(networkGroup).toMutableList() - .also { mutableGroup -> - if (index != lastItemIndex) { - mutableGroup.add(getGroupPlaceholder(index)) - } - } - } - } - - private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( - id = getGroupHeaderId(group.network), - networkName = group.network.name, - ) - - private fun createTokens(group: NetworkGroup): List { - return itemConverter.convertList(group.currencies) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt deleted file mode 100644 index 770d63c0f4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal class TokenListToListStateConverter( - private val groupsConverter: NetworkGroupToDraggableItemsConverter, - private val tokensConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensListState { - return when (value) { - is TokenList.GroupedByNetwork -> createListState(value) - is TokenList.Ungrouped -> createListState(value) - is TokenList.Empty -> createEmptyListState() - } - } - - private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork { - return OrganizeTokensListState.GroupedByNetwork( - items = groupsConverter.convertList(tokenList.groups) - .flatten() - .uniteItems() - .toPersistentList(), - ) - } - - @Suppress("UNCHECKED_CAST") // Erased type - private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { - return OrganizeTokensListState.Ungrouped( - items = tokensConverter.convertList(tokenList.currencies) - .uniteItems() - .toPersistentList() as PersistentList, - ) - } - - private fun createEmptyListState(): OrganizeTokensListState.Empty { - return OrganizeTokensListState.Empty - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt deleted file mode 100644 index 7158aa1260..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd - -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems -import com.tangem.utils.Provider -import kotlinx.collections.immutable.mutate -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull -import org.burnoutcrew.reorderable.ItemPosition - -internal class DragAndDropAdapter( - private val listStateProvider: Provider, -) : DragAndDropIntents { - - private val draggableGroupsOperations = DraggableGroupsOperations() - - private val externalListState: OrganizeTokensListState - get() = listStateProvider.invoke() - - private val dragAndDropUpdatesInternal: MutableStateFlow = MutableStateFlow(value = null) - - private var draggingItem: DraggableItem? = null - private var draggingListState: OrganizeTokensListState? = null - - val dragAndDropUpdates: Flow - get() = dragAndDropUpdatesInternal.filterNotNull() - - override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = when (val listState = externalListState) { - is OrganizeTokensListState.GroupedByNetwork -> listState.items - is OrganizeTokensListState.Empty, - is OrganizeTokensListState.Ungrouped, - -> return true // If ungrouped then item can be moved anywhere - } - - val (dragOverItem, draggingItem) = findItemsToMove( - items = items, - moveOverItemKey = dragOver.key, - movedItemKey = dragging.key, - ) - - if (dragOverItem == null || draggingItem == null) { - return false - } - - return when (draggingItem) { - is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) - is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> false - } - } - - override fun onItemDraggingStart(item: DraggableItem) { - if (draggingItem != null) return - draggingItem = item - - updateListState(DragOperation.Type.Start) { - when (item) { - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) - is DraggableItem.Token -> when (this) { - is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) - is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item) - is OrganizeTokensListState.Empty -> items - } - } - } - - draggingListState = externalListState - } - - override fun onItemDraggingEnd() { - val draggingItem = draggingItem ?: return - - updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { - when (draggingItem) { - is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - } - } - - this.draggingItem = null - } - - override fun onItemDragged(from: ItemPosition, to: ItemPosition) { - updateListState(DragOperation.Type.Dragged) { - items.mutate { - it.add(to.index, it.removeAt(from.index)) - } - } - } - - private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListState.() -> List) { - val updatedState = externalListState.updateItems { block(externalListState) } - - dragAndDropUpdatesInternal.value = DragOperation(type, updatedState) - } - - private fun findItemsToMove( - items: List, - moveOverItemKey: Any?, - movedItemKey: Any?, - ): Pair { - var moveOverItem: DraggableItem? = null - var movedItem: DraggableItem? = null - - for (item in items) { - if (item.id == moveOverItemKey) { - moveOverItem = item - } - if (item.id == movedItemKey) { - movedItem = item - } - if (moveOverItem != null && movedItem != null) { - break - } - } - - return Pair(moveOverItem, movedItem) - } - - private fun checkCanMoveHeaderOver( - moveOverItemPosition: ItemPosition, - moveOverItem: DraggableItem, - lastItemIndex: Int, - ): Boolean { - // Group item can be moved only to group divider or to ages of the items list - return when { - moveOverItemPosition.index == 0 -> true - moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.Placeholder -> true - else -> false - } - } - - private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { - // Token item can be moved only in its group - return when (moveOverItem) { - is DraggableItem.GroupHeader -> false // Token item can not be moved to group item - is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> false - } - } - - private fun checkIsItemsOrderChanged(): Boolean { - fun OrganizeTokensListState?.getItemsIds(): List? = this?.items?.mapNotNull { item -> - if (item is DraggableItem.Placeholder) { - null - } else { - item.id - } - } - - return externalListState.getItemsIds() != draggingListState.getItemsIds() - } - - data class DragOperation( - val type: Type, - val listState: OrganizeTokensListState, - ) { - - sealed class Type { - - data object Start : Type() - - data object Dragged : Type() - - data class End(val isItemsOrderChanged: Boolean) : Type() - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt deleted file mode 100644 index e92d772829..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems - -internal class DraggableGroupsOperations { - - private var groupIdToTokens: Map>? = null - - fun collapseGroup(items: List, movingGroup: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return items - - groupIdToTokens = items - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - val itemsWithoutGroupTokens = items.filterNot { - it is DraggableItem.Token && it.groupId == movingGroup.id - } - - return itemsWithoutGroupTokens.divideMovingItem(movingGroup) - } - - fun collapseGroupV2(items: List, movingGroup: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return items - - groupIdToTokens = items - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - val itemsWithoutGroupTokens = items.filterNot { - it is DraggableItem.Token && it.groupId == movingGroup.id - } - - return itemsWithoutGroupTokens.divideMovingItem(movingGroup) - } - - fun expandGroups(items: List): List { - if (groupIdToTokens.isNullOrEmpty()) return items - - val currentGroups = items.filterIsInstance() - val lastGroupIndex = currentGroups.lastIndex - - val expandedGroups = currentGroups - .flatMapIndexed { index, group -> - buildList { - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - if (index != lastGroupIndex) { - add(getGroupPlaceholder(index)) - } - } - } - .uniteItems() - - groupIdToTokens = null - - return expandedGroups - } - - fun expandGroupsV2(items: List): List { - if (groupIdToTokens.isNullOrEmpty()) return items - - val accountList = items.filterIsInstance() - val currentGroups = items.filterIsInstance() - - val expandedGroups = if (items.any { it is DraggableItem.Portfolio }) { - accountList - .asSequence() - .flatMap { account -> - buildList { - add(account) - currentGroups - .asSequence() - .filter { it.accountId == account.id } - .forEachIndexed { index, group -> - if (index == 0) { - add(getGroupPlaceholder(accountId = group.accountId, index = -1)) - } - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - add(getGroupPlaceholder(accountId = group.accountId, index = index)) - } - } - } - } else { - currentGroups - .asSequence() - .flatMapIndexed { index, group -> - buildList { - if (index == 0) { - add(getGroupPlaceholder(accountId = group.accountId, index = -1)) - } - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - add(getGroupPlaceholder(accountId = group.accountId, index = index)) - } - } - }.toList() - - groupIdToTokens = null - - return expandedGroups - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt new file mode 100644 index 0000000000..dc1d34c847 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.wallet.presentation.preview + +import androidx.compose.ui.text.SpanStyle +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledStringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM + +internal object WalletBalancePreview { + + val content: WalletBalanceUM.Content = WalletBalanceUM.Content( + id = UserWalletId("0"), + name = "My Wallet", + balanceInAppBar = combinedReference( + stringReference("1,234"), + styledStringReference( + ".56", + { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ), + stringReference(" $"), + ), + balance = combinedReference( + stringReference("1,234"), + styledStringReference( + ".56", + { + TangemTheme.typography2.headingRegular28.toSpanStyle() + }, + ), + stringReference(" $"), + ), + deviceIcon = DeviceIconUM.Stub(cardsCount = 3), + isBalanceFlickering = false, + isZeroBalance = false, + ) + + val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading( + id = UserWalletId("1"), + name = "My Wallet", + deviceIcon = DeviceIconUM.Mobile, + ) + + val error: WalletBalanceUM.Error = WalletBalanceUM.Error( + id = UserWalletId("2"), + name = "My Wallet", + deviceIcon = DeviceIconUM.Stub(cardsCount = 3), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt new file mode 100644 index 0000000000..7a61716733 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.preview + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletActionButtons +import kotlinx.collections.immutable.persistentListOf + +internal object WalletPreviewData { + + val wallets by lazy { + mapOf( + UserWalletId(stringValue = "123") to WalletBalancePreview.content, + UserWalletId(stringValue = "321") to WalletBalancePreview.loading, + UserWalletId(stringValue = "24") to WalletBalancePreview.error, + ) + } + + val actionButtons = persistentListOf( + WalletActionButtons.Buy({}, false).buttonUM, + WalletActionButtons.Swap({}, false).buttonUM, + WalletActionButtons.Sell({}, false).buttonUM, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 49f35c817d..d4a58b4046 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse @@ -18,7 +19,9 @@ import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.navigation.WalletRoute +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import javax.inject.Inject @@ -50,6 +53,14 @@ internal class DefaultWalletRouter @Inject constructor( ) } + override fun openManageTokensScreen(accountId: AccountId) { + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.ACCOUNT, + accountId = accountId, + ) + router.push(route) + } + override fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean) { router.push( AppRoute.Onboarding( @@ -142,4 +153,12 @@ internal class DefaultWalletRouter @Inject constructor( ), ) } + + override fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) { + dialogNavigation.activate( + configuration = WalletDialogConfig.TokenActionList( + actionList = tokenActionList, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index afad2d5fe7..54a04de46c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.common.routing.AppRoute import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse @@ -13,7 +14,9 @@ import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.navigation.WalletRoute +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.SharedFlow /** @@ -37,6 +40,9 @@ internal interface InnerWalletRouter { /** Open details screen */ fun openDetailsScreen(selectedWalletId: UserWalletId) + /** Open manage tokens screen */ + fun openManageTokensScreen(accountId: AccountId) + /** Open onboarding screen */ fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false) @@ -77,4 +83,7 @@ internal interface InnerWalletRouter { /** Open yield supply entry screen */ fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) + + /** Open token action sheet */ + fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) } \ No newline at end of file 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 11aaab2b47..257ce14de3 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 @@ -57,6 +57,7 @@ sealed class WalletScreenAnalyticsEvent { val theme: String, val isImported: Boolean, val referralId: String?, + val appCurrency: String, ) : MainScreen( event = "Screen opened", params = buildMap { @@ -69,6 +70,7 @@ sealed class WalletScreenAnalyticsEvent { "Seedless" } put("Wallet Type", seedPhrase) + put("App Currency", appCurrency) putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt index 5e65dfb36a..a54705233c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.tangempay.TangemPayAnalyticsEvents @@ -29,7 +29,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor( // ignore cancelled state on analytics customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore kyc not approved state on analytics - customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return + customerInfo.info.kycStatus != KycStatus.APPROVED -> return cardInfo != null && productInstance != null -> return else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed() } 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 341c2c6726..41fe676459 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 @@ -8,9 +8,8 @@ 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.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.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import javax.inject.Inject @@ -34,10 +33,29 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( } } + fun send(displayedWalletUM: WalletUM?, newNotifications: List) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val notificationsDiff = newNotifications.filter { it !in totalNotifications } + + val eventsToSend = getEvents2(notificationsDiff) + + eventsToSend.forEach { event -> + analyticsEventHandler.send(event) + } + } + private fun getEvents(warnings: List): Set { return warnings.mapNotNullTo(mutableSetOf(), ::getEvent) } + private fun getEvents2(notifications: List): Set { + return notifications.mapNotNullTo(mutableSetOf(), ::getEvent2) + } + @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { @@ -106,4 +124,53 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UpgradeHotWalletPromo -> null } } + + @Suppress("CyclomaticComplexMethod") + private fun getEvent2(notificationUM: WalletNotificationUM): AnalyticsEvent? { + return when (notificationUM) { + WalletNotificationUM.DevCard -> DevelopmentCard() + WalletNotificationUM.FailedCardValidation -> ProductSampleCard() + is WalletNotificationUM.MissingBackup -> BackupYourWallet() + is WalletNotificationUM.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + WalletNotificationUM.TestnetCard -> TestnetCard() + WalletNotificationUM.DemoCard -> DemoCard() + is WalletNotificationUM.MissingAddresses -> MissingAddresses() + is WalletNotificationUM.RateApp -> HowDoYouLikeTangem() + is WalletNotificationUM.BackupError -> BackupError() + is WalletNotificationUM.NoteMigration -> NotePromo() + is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.OnePlusOne, + ) + is WalletNotificationUM.YieldPromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + ) + is WalletNotificationUM.FinishWalletActivation -> { + val activationState = if (notificationUM.isBackupExists) { + NoticeFinishActivation.ActivationState.Unfinished + } else { + NoticeFinishActivation.ActivationState.NotStarted + } + val balanceState = when (notificationUM.type) { + WalletNotificationType.Warning -> AnalyticsParam.EmptyFull.Full + else -> AnalyticsParam.EmptyFull.Empty + } + NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) + } + is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotificationUM.PushNotifications -> PushBanner() + is WalletNotificationUM.UnlockWallets, + is WalletNotificationUM.NoAccount, + is WalletNotificationUM.LowSignatures, + WalletNotificationUM.SomeNetworksUnreachable, + is WalletNotificationUM.UsedOutdatedData, + is WalletNotificationUM.CloreMigration, + -> null + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index f52290e6d2..fdcdc567b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -4,12 +4,8 @@ import com.tangem.common.routing.AppRoute.WalletBackup import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.primaryButton -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet @@ -19,7 +15,9 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -71,6 +69,44 @@ internal class WalletWarningsSingleEventSender @Inject constructor( } } + suspend fun send( + userWalletId: UserWalletId, + displayedWalletUM: WalletUM?, + newNotifications: List, + ) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val events = newNotifications.filter { it !in totalNotifications } + + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + + events.forEach { event -> + when (event) { + is WalletNotificationUM.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotificationUM.FinishWalletActivation -> { + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.messageEffect == TangemMessageEffect.Warning && event.isBackupExists.not()) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = true + } + } + else -> Unit + } + } + } + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return if (userWallet !is UserWallet.Hot) return 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 a243ddef48..2438cafa5a 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 @@ -11,7 +11,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -49,10 +48,10 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList", "LargeClass") @ModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( - private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, @@ -71,30 +70,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - val accountStatusList by lazy { + val accountStatusListFlow by lazy { val params = SingleAccountStatusListProducer.Params(userWallet.walletId) accountDependencies.singleAccountStatusListSupplier(params) .map { it.totalFiatBalance to it.flattenCurrencies() } .map { Lce.Content(it) } } - fun tokenListFlow(): LceFlow>> { - return if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { - accountStatusList - } else { - runCatching { tokenListStore.getOrThrow(userWallet.walletId) } - .map { result -> result.map { lce -> lce.map { it.totalFiatBalance to it.flattenCurrencies() } } } - .getOrNull() - // in case of runtime change ft in tester menu - ?: accountStatusList - } - } - - // val params = SingleAccountStatusListProducer.Params(userWallet.walletId) - // val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( - // todo account just use it, after delete accountsFeatureToggles - // accountStatusListFlow, + accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), @@ -110,7 +94,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), ) { array -> 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 } @@ -149,9 +132,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addYieldPromoNotification(clickIntents, shouldShowYieldPromo) - addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) - addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) + addWarningNotifications( + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) addPushReminderNotification( clickIntents = clickIntents, 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 c7fe144bd9..de33b6fb9a 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 @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either import arrow.core.right import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.producer.SingleAccountStatusProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.card.CardTypesResolver @@ -15,7 +14,6 @@ import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase @@ -27,11 +25,10 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import javax.inject.Inject +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @ModelScoped @Suppress("LongParameterList") internal class GetSingleWalletWarningsFactory @Inject constructor( - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleAccountStatusSupplier: SingleAccountStatusSupplier, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, @@ -40,7 +37,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, ) { - private var readyForRateAppNotification = false + private var isReadyForRateAppNotification = false fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { if (userWallet !is UserWallet.Cold) { @@ -54,7 +51,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = getWalletsUseCase().conflate(), ) { maybePrimaryCurrencyStatus, isReadyToShowRating, isNeedToBackup, userWallets -> - readyForRateAppNotification = true + isReadyForRateAppNotification = true buildList { addUsedOutdatedDataNotification(maybePrimaryCurrencyStatus) @@ -120,8 +117,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( cardTypesResolver: CardTypesResolver, clickIntents: WalletClickIntents, ) { - val userHasWalletOrWallet2 = userWallets.filterIsInstance().any { - val typesResolver = it.scanResponse.cardTypesResolver + val hasWalletOrWallet2 = userWallets.filterIsInstance().any { coldWallet -> + val typesResolver = coldWallet.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } @@ -129,7 +126,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element = WalletNotification.NoteMigration( onClick = { clickIntents.onNoteMigrationButtonClick(NOTE_MIGRATION_URL) }, ), - condition = cardTypesResolver.isTangemNote() && !userHasWalletOrWallet2, + condition = cardTypesResolver.isTangemNote() && !hasWalletOrWallet2, ) addIf( @@ -191,8 +188,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( selectedWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus?, ): Boolean { - return cryptoCurrencyStatus?.currency?.network?.let { - hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) + return cryptoCurrencyStatus?.currency?.network?.let { network -> + hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) .conflate() .distinctUntilChanged() .firstOrNull() @@ -209,7 +206,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( onDislikeClick = clickIntents::onDislikeAppClick, onCloseClick = clickIntents::onCloseRateAppWarningClick, ), - condition = isReadyToShowRating && readyForRateAppNotification, + condition = isReadyToShowRating && isReadyForRateAppNotification, ) } @@ -219,7 +216,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element is WalletNotification.Warning || element is WalletNotification.NoteMigration ) { - readyForRateAppNotification = false + isReadyForRateAppNotification = false } element @@ -229,16 +226,12 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private fun getPrimaryCurrencyStatusFlow( userWallet: UserWallet, ): Flow> { - return if (accountsFeatureToggles.isFeatureEnabled) { - getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> - accountStatus.flattenCurrencies().firstOrNull() - } - .distinctUntilChanged() - .conflate() - .map { it.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) + return getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> + accountStatus.flattenCurrencies().firstOrNull() } + .distinctUntilChanged() + .conflate() + .map { it.right() } } private fun getAccountStatusFlow(userWallet: UserWallet): Flow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt new file mode 100644 index 0000000000..0988254408 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -0,0 +1,135 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.TangemSiteUrlBuilder +import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.promo.ShouldShowPromoWalletUseCase +import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.utils.extensions.addIf +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are not critical and can be stacked with each other. + */ +@ModelScoped +internal class GetWalletNotificationsCarouselFactory @Inject constructor( + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val notificationsRepository: NotificationsRepository, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + return combine( + flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) + .distinctUntilChanged(), + flow2 = notificationsRepository.getShouldShowNotification( + NotificationId.EnablePushesReminderNotification.key, + ).distinctUntilChanged(), + flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne) + .distinctUntilChanged(), + flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(), + flow5 = getWalletsUseCase().conflate(), + ) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets -> + + buildList { + addNoteMigrationNotification(userWallet, wallets, clickIntents) + addRateAppNotification(showRateAppPromo, clickIntents) + + if (userWallet.isMultiCurrency) { + addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) + addYieldPromoNotification(clickIntents, showYieldPromo) + } + + addPushNotification( + shouldShow = showPushesNotification, + isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addRateAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(isReadyToShowRating) { + WalletNotificationUM.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ) + } + } + + private fun MutableList.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ) + } + } + + private fun MutableList.addOnePlusOnePromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.OnePlusOnePromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) }, + onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) }, + ) + } + } + + private fun MutableList.addNoteMigrationNotification( + userWallet: UserWallet, + userWallets: List, + clickIntents: WalletClickIntents, + ) { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val isUserHasWalletOrWallet2 = userWallets.filterIsInstance().any { wallet -> + val typesResolver = wallet.scanResponse.cardTypesResolver + typesResolver.isTangemWallet() || typesResolver.isWallet2() + } + + addIf(cardTypesResolver != null && cardTypesResolver.isTangemNote() && !isUserHasWalletOrWallet2) { + WalletNotificationUM.NoteMigration( + onClick = { clickIntents.onNoteMigrationButtonClick(TangemSiteUrlBuilder.NOTE_MIGRATION_URL) }, + ) + } + } + + private fun MutableList.addPushNotification( + shouldShow: Boolean, + isPushesAllowed: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(shouldShow && !isPushesAllowed) { + WalletNotificationUM.PushNotifications( + onCloseClick = clickIntents::onDenyPermissions, + onEnabledClick = clickIntents::onAllowPermissions, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt new file mode 100644 index 0000000000..8735cf42f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -0,0 +1,329 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are critical and should be shown separately from each other. + */ +@Suppress("LongParameterList") +@ModelScoped +internal class GetWalletNotificationsFactory @Inject constructor( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, + private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, + private val accountDependencies: AccountDependencies, + private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + + return combine( + flow = accountStatusListFlow, + flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), + flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), + flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + ) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped -> + val totalFiatBalance = accountList.totalFiatBalance + val flattenCurrencies = accountList.flattenCurrencies() + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification( + userWallet = userWallet, + totalFiatBalance = totalFiatBalance, + clickIntents = clickIntents, + shouldAccessCodeSkipped = shouldAccessCodeSkipped, + ) + + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) + + addWarningNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { + addIf( + element = WalletNotificationUM.UsedOutdatedData, + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, + ) + } + + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + if (userWallet !is UserWallet.Cold) { + return + } + + addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + + addIf( + element = WalletNotificationUM.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotificationUM.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotificationUM.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.DemoCard, + condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + userWallet: UserWallet, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val currencies = flattenCurrencies.getMissingAddressCurrencies().ifEmpty { return } + + addIf( + element = WalletNotificationUM.MissingAddresses( + tangemIcon = walletInterationIcon(userWallet), + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun List.getMissingAddressCurrencies(): List { + return this + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.MissingBackup( + onClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotificationUM.TestnetCard, + condition = cardTypesResolver?.isTestCard() == true, + ) + + addIf( + element = WalletNotificationUM.SomeNetworksUnreachable, + condition = flattenCurrencies.hasUnreachableNetworks(), + ) + + addCloreMigrationNotification(userWallet, flattenCurrencies, clickIntents) + + addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull()) + + addIf( + element = WalletNotificationUM.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, flattenCurrencies.firstOrNull()), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotificationUM.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private fun MutableList.addCloreMigrationNotification( + userWallet: UserWallet, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + addIf( + condition = userWallet.isMultiCurrency, + element = WalletNotificationUM.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } + } + + private fun List.hasUnreachableNetworks(): Boolean { + return any { it.value is CryptoCurrencyStatus.Unreachable } + } + + private fun MutableList.addFinishWalletActivationNotification( + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + clickIntents: WalletClickIntents, + shouldAccessCodeSkipped: Boolean, + ) { + if (userWallet !is UserWallet.Hot) return + + val isBackupExists = userWallet.backedUp + val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && + !shouldAccessCodeSkipped + val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired + + val messageEffect = when (totalFiatBalance) { + TotalFiatBalance.Failed, + TotalFiatBalance.Loading, + -> TangemMessageEffect.None + is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) { + TangemMessageEffect.Warning + } else { + TangemMessageEffect.None + } + } + + addIf( + element = WalletNotificationUM.FinishWalletActivation( + messageEffect = messageEffect, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, + isBackupExists = isBackupExists, + ), + condition = shouldShowFinishActivation, + ) + } + + private fun MutableList.addSeedNotificationIfNeeded( + userWallet: UserWallet.Cold, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + val isNotificationAvailable = with(userWallet) { + val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) + val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported + + !isDemo && isWalletWithSeedPhrase + } + + when (seedPhraseIssueStatus) { + SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( + element = WalletNotificationUM.SeedPhraseNotification( + onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, + onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( + element = WalletNotificationUM.SeedPhraseSecondNotification( + onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, + onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + if (selectedWallet !is UserWallet.Cold || !selectedWallet.isMultiCurrency) return false + val network = cryptoCurrencyStatus?.currency?.network ?: return false + + return hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) + .conflate() + .distinctUntilChanged() + .firstOrNull() == true + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt index 088110a3cb..cb7eb1f324 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt @@ -1,14 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -27,18 +28,23 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( return@map false } - return@map walletManagersFacade.validateSignatureCount( - userWalletId = userWallet.walletId, - network = network, - signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, - ) - .fold( - ifLeft = { true }, - ifRight = { - cardRepository.setCardWasScanned(cardId = userWallet.cardId) - false - }, + return@map try { + walletManagersFacade.validateSignatureCount( + userWalletId = userWallet.walletId, + network = network, + signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, ) + .fold( + ifLeft = { true }, + ifRight = { + cardRepository.setCardWasScanned(cardId = userWallet.cardId) + false + }, + ) + } catch (e: IllegalArgumentException) { + Timber.w(e, "Unable to validate signature count: user wallet not found") + false + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt deleted file mode 100644 index 1aa129675c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.error.TokenListError -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.shareIn -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject - -@ModelScoped -internal class MultiWalletTokenListStore @Inject constructor( - private val getTokenListUseCase: GetTokenListUseCase, -) { - - private val flows: ConcurrentHashMap> by lazy { - ConcurrentHashMap() - } - - fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) { - if (flows[userWalletId] != null) { - Timber.d("Flow with token list for $userWalletId already exists") - return - } - - coroutineScope.ensureActive() - - flows[userWalletId] = getTokenListUseCase - .launch(userWalletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - - Timber.d("Flow with token list for $userWalletId created") - } - - fun getOrThrow(userWalletId: UserWalletId): LceFlow { - return requireNotNull(flows[userWalletId]) { - "Flow with token list for $userWalletId doesn't exist" - } - } - - fun remove(userWalletId: UserWalletId) { - flows.remove(userWalletId) - - Timber.d("Flow with token list for $userWalletId removed") - } - - fun clear() { - flows.clear() - - Timber.d("All flows with token list cleared") - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt index 46e14c7693..10de4aeeb6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -1,7 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveInAndJoin @@ -27,6 +28,7 @@ import javax.inject.Singleton internal class WalletContentFetcher @Inject constructor( private val walletBalanceFetcher: WalletBalanceFetcher, private val dispatchers: CoroutineDispatcherProvider, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, ) { private val fetchingJobMap = ConcurrentHashMap() @@ -64,8 +66,12 @@ internal class WalletContentFetcher @Inject constructor( Timber.d("Start fetching for $userWalletId") val maybeResult = launch { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) + walletBalanceFetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), + ).onLeft(Timber::e) } .saveInAndJoin(jobHolder) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index eb3e310843..1d84273c15 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -17,6 +17,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ +@Deprecated("Will be removed in favor of getWalletIconUseCase") internal class WalletImageResolver @Inject constructor( private val walletsRepository: WalletsRepository, ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt index 4d9bf94d7e..bfac80ba45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -1,51 +1,40 @@ package com.tangem.feature.wallet.presentation.wallet.loaders import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.* import javax.inject.Inject @Suppress("LongParameterList") @ModelScoped internal class WalletContentLoaderFactory @Inject constructor( - private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, - private val multiWalletContentLoaderV2Factory: MultiWalletContentLoaderV2.Factory, - private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, - private val singleWalletWithTokenContentLoaderV2Factory: SingleWalletWithTokenContentLoaderV2.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, - private val singleWalletContentLoaderV2Factory: SingleWalletContentLoaderV2.Factory, + private val multiWalletContentLoaderFactory: MultiWalletContentLoader.Factory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoader.Factory, + private val singleWalletContentLoaderLegacyFactory: SingleWalletContentLoaderLegacy.Factory, + private val singleWalletContentLoader: SingleWalletContentLoader.Factory, + private val designFeatureToggles: DesignFeatureToggles, ) { - fun create( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - ): WalletContentLoader? { + fun create(userWallet: UserWallet, isRefresh: Boolean = false): WalletContentLoader? { return when { userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - multiWalletContentLoaderV2Factory.create(userWallet) - } else { - multiWalletContentLoaderFactory.create(userWallet, clickIntents) - } + multiWalletContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletWithTokenContentLoaderV2Factory.create(userWallet) + if (designFeatureToggles.isRedesignEnabled) { + singleWalletContentLoader.create(userWallet) } else { - singleWalletWithTokenContentLoaderFactory.create(userWallet, clickIntents) + singleWalletWithTokenContentLoaderFactory.create(userWallet) } } userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletContentLoaderV2Factory.create(userWallet, isRefresh) + if (designFeatureToggles.isRedesignEnabled) { + singleWalletContentLoader.create(userWallet) } else { - singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh) + singleWalletContentLoaderLegacyFactory.create(userWallet, isRefresh) } } else -> null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt index 1fa83b51e4..ee6cde3b58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -18,8 +18,8 @@ internal class WalletLoaderStorage @Inject constructor() { } fun remove(id: UserWalletId) { - loaders[id]?.let { - it.forEach(Job::cancel) + loaders[id]?.let { jobs -> + jobs.forEach(Job::cancel) loaders.remove(id) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt index dd68293363..8965195057 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -4,7 +4,6 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.newSingleThreadContext @@ -14,9 +13,8 @@ import javax.inject.Inject /** * Base wallet screen content loader. Use it to load content by [UserWallet]. * - * @property factory factory that creates loader - * @property storage storage that save loader's jobs - * @property dispatchers coroutine dispatchers provider + * @property factory factory that creates loader + * @property storage storage that save loader's jobs * [REDACTED_AUTHOR] */ @@ -33,25 +31,19 @@ internal class WalletScreenContentLoader @Inject constructor( * Load content by [UserWallet] * * @param userWallet user wallet - * @param clickIntents click intents * @param isRefresh flag that determinate if content must load again * @param coroutineScope coroutine scope */ - fun load( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - coroutineScope: CoroutineScope, - ) { + fun load(userWallet: UserWallet, isRefresh: Boolean = false, coroutineScope: CoroutineScope) { if (userWallet.isLocked) return val id = userWallet.walletId if (!storage.contains(id)) { - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) + loadInternal(userWallet, coroutineScope, isRefresh) } else { if (isRefresh) { storage.remove(id) - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) + loadInternal(userWallet, coroutineScope, isRefresh = true) } else { Timber.d("$id content loading has already started") } @@ -70,15 +62,9 @@ internal class WalletScreenContentLoader @Inject constructor( singleBackgroundDispatcher.close() } - private fun loadInternal( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - coroutineScope: CoroutineScope, - isRefresh: Boolean, - ) { + private fun loadInternal(userWallet: UserWallet, coroutineScope: CoroutineScope, isRefresh: Boolean) { val loader = factory.create( userWallet = userWallet, - clickIntents = clickIntents, isRefresh = isRefresh, ) 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 c71fb95879..69217500ac 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 @@ -1,96 +1,40 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -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.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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.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.tangempay.TangemPayFeatureToggles +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject @Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2 instead") -@ModelScoped -internal class MultiWalletContentLoader( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val tokenListStore: MultiWalletTokenListStore, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - 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 tangemPayFeatureToggles: TangemPayFeatureToggles, +internal class MultiWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val accountListSubscriberFactory: AccountListSubscriber.Factory, + private val walletNFTListSubscriberFactory: WalletNFTListSubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, + private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, + private val designFeatureToggles: DesignFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - MultiWalletTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) + override fun create(): List = listOf( + accountListSubscriberFactory.create(userWallet), + walletNFTListSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + if (designFeatureToggles.isRedesignEnabled) { + walletNotificationsSubscriberFactory.create(userWallet) + } else { + multiWalletWarningsSubscriberFactory.create(userWallet) + }, + multiWalletActionButtonsSubscriberFactory.create(userWallet), + tangemPayMainSubscriberFactory.create(userWallet), + ) - WalletNFTListSubscriber( - userWallet = userWallet, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - stateHolder = stateHolder, - walletsRepository = walletsRepository, - clickIntents = clickIntents, - currenciesRepository = currenciesRepository, - ).let(::add) - - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - add(tangemPayMainSubscriberFactory.create(userWallet)) - } - } + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletContentLoader } } \ No newline at end of file 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 deleted file mode 100644 index 525899d92e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -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.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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.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.TangemPayMainSubscriber -import com.tangem.features.tangempay.TangemPayFeatureToggles -import javax.inject.Inject - -@Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2.Factory instead") -@ModelScoped -internal class MultiWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val walletsRepository: WalletsRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val currenciesRepository: CurrenciesRepository, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) { - - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { - return MultiWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - getStoryContentUseCase = getStoryContentUseCase, - walletsRepository = walletsRepository, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - currenciesRepository = currenciesRepository, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - tangemPayFeatureToggles = tangemPayFeatureToggles, - tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file 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 deleted file mode 100644 index 2c52604748..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ /dev/null @@ -1,63 +0,0 @@ -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.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.tangempay.TangemPayFeatureToggles -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class MultiWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet, - private val accountListSubscriberFactory: AccountListSubscriber.Factory, - private val tokenListAnalyticsSubscriberFactory: TokenListAnalyticsSubscriber.Factory, - private val walletNFTListSubscriberV2Factory: WalletNFTListSubscriberV2.Factory, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOfNotNull( - accountListSubscriberFactory.create(userWallet = userWallet), - tokenListAnalyticsSubscriberFactory.create(userWallet = userWallet), - walletNFTListSubscriberV2Factory.create(userWallet = userWallet), - checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - getStoryContentUseCase = getStoryContentUseCase, - ), - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - tangemPayMainSubscriberFactory.create(userWallet) - } else { - null - }, - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet): MultiWalletContentLoaderV2 - } -} \ No newline at end of file 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 34c986c644..22f51e58d5 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 @@ -1,83 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -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.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.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletNotificationsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -@Suppress("LongParameterList") -internal class SingleWalletContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, +/** + * This content loader is used for the wallet screen when for single wallets. + * For example - [Note, Twins, Single with token] + */ +internal class SingleWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + private val walletNotificationsSubscriber: WalletNotificationsSubscriber.Factory, + private val singleWalletSubscriber: SingleWalletSubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return listOf( - PrimaryCurrencySubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ), - TxHistorySubscriber( - userWallet = userWallet, - isRefresh = isRefresh, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - ), - ) + override fun create(): List = listOf( + singleWalletSubscriber.create(userWallet = userWallet), + walletNotificationsSubscriber.create(userWallet = userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletContentLoader } } \ No newline at end of file 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 deleted file mode 100644 index 4340bf544b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -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.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 javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletContentLoaderV2.Factory instead") -internal class SingleWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { - return SingleWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - isRefresh = isRefresh, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt new file mode 100644 index 0000000000..27228fe3bc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +@Suppress("LongParameterList") +internal class SingleWalletContentLoaderLegacy @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + private val primaryCurrencySubscriberFactory: PrimaryCurrencySubscriber.Factory, + private val singleWalletButtonsSubscriberFactory: SingleWalletButtonsSubscriber.Factory, + private val singleWalletNotificationsSubscriberFactory: SingleWalletNotificationsSubscriber.Factory, + private val singleWalletExpressStatusesSubscriberFactory: SingleWalletExpressStatusesSubscriber.Factory, + private val txHistorySubscriberLegacyFactory: TxHistorySubscriberLegacy.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List = listOf( + primaryCurrencySubscriberFactory.create(userWallet), + singleWalletButtonsSubscriberFactory.create(userWallet), + singleWalletNotificationsSubscriberFactory.create(userWallet), + singleWalletExpressStatusesSubscriberFactory.create(userWallet), + txHistorySubscriberLegacyFactory.create(userWallet, isRefresh), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderLegacy + } +} \ 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 deleted file mode 100644 index 6c4711cb5f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -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.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 -import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory -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.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class SingleWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - @Assisted private val isRefresh: Boolean, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val accountDependencies: AccountDependencies, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val dispatchers: CoroutineDispatcherProvider, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - PrimaryCurrencySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - stateController = stateHolder, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - stateController = stateHolder, - clickIntents = clickIntents, - getCryptoCurrencyActionsUseCaseV2 = getCryptoCurrencyActionsUseCaseV2, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - stateController = stateHolder, - clickIntents = clickIntents, - analyticsEventHandler = analyticsEventHandler, - ), - TxHistorySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - isRefresh = isRefresh, - stateController = stateHolder, - clickIntents = clickIntents, - ), - CheckWalletWithFundsSubscriber( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - walletWithFundsChecker = walletWithFundsChecker, - dispatchers = dispatchers, - ), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderV2 - } -} \ No newline at end of file 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 a43c2c130e..a0d3b6344f 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 @@ -1,70 +1,30 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -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.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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.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.MultiWalletActionButtonsSubscriber +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.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriberLegacy import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -@Deprecated("Use SingleWalletWithTokenContentLoaderV2 instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class SingleWalletWithTokenContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + private val singleWalletWithTokenSubscriberLegacyFactory: SingleWalletWithTokenSubscriberLegacy.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - SingleWalletWithTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + override fun create(): List = listOf( + singleWalletWithTokenSubscriberLegacyFactory.create(userWallet), + multiWalletWarningsSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoader } } \ 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 deleted file mode 100644 index a0fcbbd771..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -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.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import javax.inject.Inject - -// TODO: Refactor -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletWithTokenContentLoaderV2.Factory instead") -@ModelScoped -internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { - return SingleWalletWithTokenContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - getStoryContentUseCase = getStoryContentUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file 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 deleted file mode 100644 index f11999ca24..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.domain.models.wallet.UserWallet -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.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 - -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val clickIntents: WalletClickIntents, - private val stateController: WalletStateController, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - singleWalletWithTokenSubscriberFactory.create(userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - checkWalletWithFundsSubscriberFactory.create(userWallet), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoaderV2 - } -} \ No newline at end of file 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 a39274838f..b1ddbd67b7 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 @@ -1,12 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer @@ -25,7 +23,9 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor() { +internal class WalletStateController @Inject constructor( + private val designFeatureToggles: DesignFeatureToggles, +) { val uiState: StateFlow get() = mutableUiState @@ -53,6 +53,10 @@ internal class WalletStateController @Inject constructor() { return value.wallets.firstOrNull { it.walletCardState.id == userWalletId } } + fun getWalletUM(userWalletId: UserWalletId): WalletUM? { + return value.wallets2.firstOrNull { it.walletsBalanceUM.id == userWalletId } + } + fun getWalletStateIfSelected(walletId: UserWalletId): WalletState? { val selectedWalletId = getSelectedWalletId() @@ -61,16 +65,40 @@ internal class WalletStateController @Inject constructor() { } } + fun getWalletUMIfSelected(walletId: UserWalletId): WalletUM? { + val selectedWalletId = getSelectedWalletId() + + return value.wallets2.firstOrNull { + it.walletsBalanceUM.id == walletId && it.walletsBalanceUM.id == selectedWalletId + } + } + fun getSelectedWallet(): WalletState { return with(value) { wallets[selectedWalletIndex] } } + fun getSelectedWalletUM(): WalletUM { + return with(value) { wallets2[selectedWalletIndex] } + } + fun getSelectedWalletId(): UserWalletId { - return with(value) { wallets[selectedWalletIndex].walletCardState.id } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2[selectedWalletIndex].walletsBalanceUM.id + } else { + wallets[selectedWalletIndex].walletCardState.id + } + } } fun getWalletIndexByWalletId(userWalletId: UserWalletId): Int? { - return with(value) { wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2.indexOfFirstOrNull { it.walletsBalanceUM.id == userWalletId } + } else { + wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } + } + } } fun showBottomSheet( @@ -105,12 +133,12 @@ internal class WalletStateController @Inject constructor() { topBarConfig = WalletTopBarConfig(onDetailsClick = {}), selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, wallets = persistentListOf(), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt deleted file mode 100644 index 84ec1221bd..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -/** - * Config for the token actions bottom sheet - * - * @property actions actions - */ -internal data class ActionsBottomSheetConfig( - val actions: ImmutableList, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt index 12c3c648f1..1b678f372d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.annotation.DrawableRes import com.tangem.core.ui.extensions.TextReference +import kotlinx.serialization.Serializable /** * Action button config @@ -10,12 +11,13 @@ import com.tangem.core.ui.extensions.TextReference * @property iconResId icon resource id * @property onClick lambda be invoked when action component is clicked * @property isWarning if warning row - * @property enabled enabled + * @property isEnabled enabled */ -data class TokenActionButtonConfig( +@Serializable +data class TokenActionButtonUM( val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val isWarning: Boolean, - val enabled: Boolean = true, + val isEnabled: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt new file mode 100644 index 0000000000..512f16d709 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R + +/** + * Model for action buttons on the wallet card. It contains the button's text, icon, click listener, and enabled state. + */ +@Immutable +internal sealed class WalletActionButtons( + private val text: TextReference, + @DrawableRes private val iconRes: Int, +) { + + abstract val onClick: () -> Unit + + abstract val isEnabled: Boolean + + val buttonUM: TangemButtonUM + get() = TangemButtonUM( + text = text, + iconRes = iconRes, + type = TangemButtonType.Secondary, + shape = TangemButtonShape.Rounded, + onClick = onClick, + isEnabled = isEnabled, + state = if (isEnabled) { + TangemButtonState.Default + } else { + TangemButtonState.Disabled + }, + ) + + data class Buy( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_buy), + iconRes = R.drawable.ic_plus_default_24, + ) + + data class Swap( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_swap), + iconRes = R.drawable.ic_exchange_default_24, + ) + + data class Sell( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_sell), + iconRes = R.drawable.ic_dollar_default_24, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt new file mode 100644 index 0000000000..b3a4710b3a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents the state of the wallet balance in the UI. + * + * The sealed interface has three implementations: + * - [Content]: Represents the state when the wallet balance is successfully loaded. + * - [Error]: Represents the state when there was an error loading the wallet balance. + * - [Loading]: Represents the state when the wallet balance is currently being loaded. + * + * @property id The unique identifier of the wallet. + * @property name The name of the wallet. + */ +@Immutable +internal sealed interface WalletBalanceUM { + + /** Wallet Id */ + val id: UserWalletId + + /** Wallet Name */ + val name: String + + /** Wallet Icon */ + val deviceIcon: DeviceIconUM + + /** + * Wallet card content state + * + * @property id wallet id + * @property name wallet name + * @property balance wallet balance + */ + data class Content( + override val id: UserWalletId, + override val name: String, + override val deviceIcon: DeviceIconUM, + val balance: TextReference, + val balanceInAppBar: TextReference, + val isBalanceFlickering: Boolean, + val isZeroBalance: Boolean?, + ) : WalletBalanceUM + + /** + * Wallet card error state + * + * @property id wallet id + * @property name wallet name + */ + data class Error( + override val id: UserWalletId, + override val name: String, + override val deviceIcon: DeviceIconUM, + ) : WalletBalanceUM + + /** + * Wallet card loading state + * + * @property id wallet id + * @property name wallet name + */ + data class Loading( + override val id: UserWalletId, + override val name: String, + override val deviceIcon: DeviceIconUM, + ) : WalletBalanceUM + + fun copySealed(name: String): WalletBalanceUM { + return when (this) { + is Content -> copy(name = name) + is Error -> copy(name = name) + is Loading -> copy(name = name) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt deleted file mode 100644 index adfede4a1c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.wallet.impl.R - -/** - * Wallet bottom sheet config - * -[REDACTED_AUTHOR] - */ -sealed class WalletBottomSheetConfig( - open val title: TextReference, - open val subtitle: TextReference, - @DrawableRes open val iconResId: Int, - val primaryButtonConfig: ButtonConfig, - val secondaryButtonConfig: ButtonConfig, -) : TangemBottomSheetConfigContent { - - data class ButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - @DrawableRes val iconResId: Int? = null, - ) - - data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig( - title = resourceReference(id = R.string.common_access_denied), - subtitle = resourceReference( - id = R.string.unlock_wallet_description_full, - formatArgs = wrappedList( - resourceReference(R.string.common_biometrics), - ), - ), - iconResId = R.drawable.ic_locked_24, - primaryButtonConfig = ButtonConfig( - text = resourceReference( - id = R.string.user_wallet_list_unlock_all_with, - formatArgs = wrappedList(resourceReference(R.string.common_biometrics)), - ), - onClick = onUnlockClick, - ), - secondaryButtonConfig = ButtonConfig( - text = resourceReference(id = R.string.welcome_unlock_card), - onClick = onScanClick, - iconResId = R.drawable.ic_tangem_24, - ), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index a482dc420f..e1144d4983 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.details.TokenAction +import kotlinx.collections.immutable.ImmutableList import kotlinx.serialization.Serializable /** @@ -26,6 +27,11 @@ internal sealed interface WalletDialogConfig { @Serializable data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig + @Serializable + data class TokenActionList( + val actionList: ImmutableList, + ) : WalletDialogConfig + @Serializable data class YieldSupplyWarning( val cryptoCurrency: CryptoCurrency, 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 4e3b74f4cf..d26b02f2f7 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 @@ -31,4 +31,6 @@ internal sealed class WalletEvent { val onAllow: () -> Unit, val onDeny: () -> Unit, ) : WalletEvent() + + data object CollapseBalance : WalletEvent() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt new file mode 100644 index 0000000000..9d24a0c3da --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -0,0 +1,496 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageButtonUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.persistentListOf + +/** + * Wallet notification types + */ +internal enum class WalletNotificationType { + Status, + Critical, + Warning, + Promo, + Survey, + Informational, +} + +/** + * Wallet notification UI model + * + * @property messageUM - message to show in notification + * @property type - type of notification, affects design and priority + */ +internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val type: WalletNotificationType) { + + // region Status + data object SomeNetworksUnreachable : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SomeNetworksUnreachableNotification", + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data object UsedOutdatedData : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UsedOutdatedDataNotification", + title = stringReference("Missing some token balances"), // todo redesign main lokalise + subtitle = stringReference("Will be updated as soon as possible"), // todo redesign main lokalise + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data object FailedCardValidation : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FailedCardValidationNotification", + title = resourceReference(id = R.string.warning_failed_to_verify_card_title), + subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + messageEffect = TangemMessageEffect.Warning, + ), + type = WalletNotificationType.Status, + ) + + data object DevCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DevCardNotification", + title = resourceReference(id = R.string.warning_developer_card_title), + subtitle = resourceReference(id = R.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object TestnetCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TestnetCardNotification", + title = resourceReference(id = R.string.warning_testnet_card_title), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object DemoCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DemoCardNotification", + title = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + // endregion + + // region Critical + data class BackupError(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "BackupErrorNotification", + title = resourceReference(id = R.string.warning_backup_errors_title), + subtitle = resourceReference(id = R.string.warning_backup_errors_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_contact_support), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class SeedPhraseNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_issue_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_issue_message), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Critical, + ) + + data class SeedPhraseSecondNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseSecondIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_action_required_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + + ), + type = WalletNotificationType.Critical, + ) + + data class MissingBackup(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingBackupNotification", + title = resourceReference(id = R.string.warning_no_backup_title), + subtitle = resourceReference(id = R.string.warning_no_backup_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.button_start_backup_process), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class LowSignatures(val count: Int) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "LowSignaturesNotification", + title = resourceReference(id = R.string.warning_low_signatures_title), + subtitle = resourceReference( + id = R.string.warning_low_signatures_message, + formatArgs = wrappedList(count.toString()), + ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Critical, + ) + + data class FinishWalletActivation( + val messageEffect: TangemMessageEffect, + val isBackupExists: Boolean, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FinishWalletActivationNotification", + title = resourceReference(R.string.hw_activation_need_title), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { + when (messageEffect) { + TangemMessageEffect.Warning -> TangemTheme.colors2.graphic.neutral.primary + else -> TangemTheme.colors2.graphic.status.attention + } + }, + ), + messageEffect = messageEffect, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.hw_activation_need_finish), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = when (messageEffect) { + TangemMessageEffect.Warning -> WalletNotificationType.Critical + else -> WalletNotificationType.Warning + }, + ) + + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NumberOfSignedHashesIncorrectNotification", + title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Critical, + ) + // endregion + + // region Warning + data class MissingAddresses( + @DrawableRes val tangemIcon: Int?, + val missingAddressesCount: Int, + val onGenerateClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingAddressesNotification", + title = resourceReference(id = R.string.warning_missing_derivation_title), + subtitle = pluralReference( + id = R.plurals.warning_missing_derivation_message, + count = missingAddressesCount, + formatArgs = wrappedList(missingAddressesCount), + ), + isCentered = true, + messageEffect = TangemMessageEffect.Card, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_generate_addresses), + type = TangemButtonType.Primary, + iconRes = tangemIcon, + onClick = onGenerateClick, + ), + ), + ), + type = WalletNotificationType.Warning, + ) + + data class NoAccount(val network: String, val symbol: String, val amount: String) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoAccountNotification", + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference( + id = R.string.no_account_generic, + wrappedList(network, amount, symbol), + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data class UnlockWallets(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UnlockWalletsNotification", + title = resourceReference(id = R.string.common_access_denied), + subtitle = resourceReference( + id = R.string.warning_access_denied_message, + formatArgs = wrappedList( + resourceReference(R.string.common_biometrics), + ), + ), + onClick = onClick, + messageEffect = TangemMessageEffect.Card, + isCentered = true, + ), + type = WalletNotificationType.Warning, + ) + // endregion + + // region Promo + data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoteMigrationNotification", + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + messageEffect = TangemMessageEffect.Magic, + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + type = TangemButtonType.Primary, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class OnePlusOnePromo( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "OnePlusOnePromoNotification", + title = resourceReference(R.string.notification_one_plus_one_title), + subtitle = resourceReference(R.string.notification_one_plus_one_text), + messageEffect = TangemMessageEffect.Magic, + onCloseClick = onCloseClick, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.Primary, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "YieldPromoNotification", + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_yield_promo_button), + type = TangemButtonType.Primary, + onClick = onTermsAndConditionsClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + // endregion + + // region Survey + data class RateApp( + val onLikeClick: () -> Unit, + val onDislikeClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "RateAppNotification", + title = resourceReference(id = R.string.warning_rate_app_title), + subtitle = resourceReference(id = R.string.warning_rate_app_message), + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_could_be_better), + type = TangemButtonType.PrimaryInverse, + onClick = onDislikeClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_like_it), + type = TangemButtonType.Primary, + onClick = onLikeClick, + ), + ), + messageEffect = TangemMessageEffect.None, + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Survey, + ) + // endregion + + // region Informational + data class PushNotifications( + val onCloseClick: () -> Unit, + val onEnabledClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "PushNotificationsNotification", + title = resourceReference(R.string.user_push_notification_banner_title), + subtitle = resourceReference(R.string.user_push_notification_banner_subtitle), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.common_later), + type = TangemButtonType.PrimaryInverse, + onClick = onCloseClick, + ), + TangemMessageButtonUM( + text = resourceReference(R.string.common_enable), + type = TangemButtonType.Primary, + onClick = onEnabledClick, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + + data class CloreMigration( + val onStartMigrationClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "CloreMigrationNotification", + title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), + onClick = onStartMigrationClick, + type = TangemButtonType.PrimaryInverse, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + // endregion +} \ 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 9c6f2658ae..fe7cb111e8 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 @@ -9,10 +9,10 @@ internal data class WalletScreenState( val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, + val wallets2: ImmutableList, val onWalletChange: (index: Int, onlyState: Boolean) -> Unit, val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, - val isNewMarketEnabled: Boolean, 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/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 20023e6233..12261f3059 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -55,11 +55,6 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty } - - enum class WalletType { - Hot, - Cold, - } } sealed class SingleCurrency : WalletState, TxHistoryStateHolder { @@ -96,4 +91,9 @@ internal sealed interface WalletState : WalletStateHolder { override val marketPriceBlockState: MarketPriceBlockState? = null } } +} + +enum class WalletType { + Hot, + Cold, } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt new file mode 100644 index 0000000000..91ad3be6ab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * State of the tokens list in the wallet screen + * + * @property tokenList list of tokens to display + * @property organizeButtonUM configuration for the "Organize Tokens" button, if it should + */ +@Immutable +internal sealed class WalletTokensListUM { + + abstract val tokenList: ImmutableList + abstract val organizeButtonUM: TangemButtonUM? + + data object Empty : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf() + override val organizeButtonUM: TangemButtonUM? = null + } + + data object Loading : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf( + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "0"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "1"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "2"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + ) + override val organizeButtonUM: TangemButtonUM? = null + } + + data class Content( + override val tokenList: ImmutableList, + override val organizeButtonUM: TangemButtonUM?, + ) : WalletTokensListUM() +} + +/** + * State of token list item in the wallet screen + */ +@Immutable +internal sealed interface TokensListItemUM2 { + val tokenRowUM: TangemRowUM + + data class GroupTitle( + override val tokenRowUM: TangemHeaderRowUM, + ) : TokensListItemUM2 + + data class Token( + override val tokenRowUM: TangemTokenRowUM, + ) : TokensListItemUM2 + + data class Portfolio( + override val tokenRowUM: TangemTokenRowUM, + val tokenList: ImmutableList, + val isExpanded: Boolean, + val isCollapsable: Boolean, + ) : TokensListItemUM2 +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt new file mode 100644 index 0000000000..d7b3a5af2d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.ds.button.TangemButtonUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed interface WalletUM { + + val pullToRefreshConfig: PullToRefreshConfig + val walletsBalanceUM: WalletBalanceUM + + val buttons: PersistentList + val notifications: ImmutableList + val notificationsCarousel: ImmutableList + + val tokensListUM: WalletTokensListUM + + val nftState: WalletNFTItemUM + + val type: WalletType + + val tangemPayState: TangemPayState + + data class Content( + override val pullToRefreshConfig: PullToRefreshConfig, + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val notifications: ImmutableList, + override val notificationsCarousel: ImmutableList, + override val tokensListUM: WalletTokensListUM, + override val nftState: WalletNFTItemUM, + override val type: WalletType, + override val tangemPayState: TangemPayState, + ) : WalletUM + + data class Locked( + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val type: WalletType, + override val notifications: ImmutableList = persistentListOf(), + ) : WalletUM { + override val notificationsCarousel: ImmutableList = persistentListOf() + override val pullToRefreshConfig = PullToRefreshConfig(false, {}) + override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Empty // todo redesign main locked state + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden + override val tangemPayState: TangemPayState = TangemPayState.Empty + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index e120fec1a1..ec3d6943be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -11,18 +12,21 @@ internal class AddWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(), + wallets2 = (prevState.wallets2 + walletLoadingStateFactory.create2(userWallet)).toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt index a76c101e75..3380c53a2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { @@ -22,6 +23,10 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig(prevState: WalletState) = prevState.bottomSheetConfig?.copy( isShown = false, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index 3b6aeb35c8..f30c88927b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -13,19 +14,34 @@ internal class DeleteWalletTransformer( override fun transform(prevState: WalletScreenState): WalletScreenState { val deletedWalletState = prevState.getDeletedWalletState() + val deletedWalletUM = prevState.getDeletedWalletState2() - if (deletedWalletState == null) { - Timber.e("Wallets does not contain deleted wallet") - return prevState + return when { + deletedWalletUM != null && deletedWalletState != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), + ) + deletedWalletUM != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), + ) + deletedWalletState != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + ) + else -> { + Timber.e("Wallets does not contain deleted wallet") + prevState + } } - - return prevState.copy( - selectedWalletIndex = selectedWalletIndex, - wallets = (prevState.wallets - deletedWalletState).toImmutableList(), - ) } private fun WalletScreenState.getDeletedWalletState(): WalletState? { return wallets.firstOrNull { it.walletCardState.id == deletedWalletId } } + + private fun WalletScreenState.getDeletedWalletState2(): WalletUM? { + return wallets2.firstOrNull { it.walletsBalanceUM.id == deletedWalletId } + } } \ 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 559a45db9d..eb010c9a39 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 @@ -1,30 +1,37 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState.MultiCurrency.WalletType import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWallet +import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList internal class InitializeWalletsTransformer( private val selectedWalletIndex: Int, private val wallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } @@ -43,6 +50,9 @@ internal class InitializeWalletsTransformer( } } .toImmutableList(), + wallets2 = wallets + .map(::createInitState) + .toImmutableList(), onWalletChange = clickIntents::onWalletChange, onDismissMarketsTooltip = clickIntents::onDismissMarketsTooltip, ) @@ -80,6 +90,16 @@ internal class InitializeWalletsTransformer( ) } + private fun createInitState(userWallet: UserWallet): WalletUM { + return if (userWallet.isLocked) { + userWallet.toLockedWalletUM() + } else { + walletLoadingStateFactory.create2( + userWallet = userWallet, + ) + } + } + private fun UserWallet.toLockedWalletCardState(): WalletCardState { return WalletCardState.LockedContent( id = walletId, @@ -90,6 +110,27 @@ internal class InitializeWalletsTransformer( ) } + private fun UserWallet.toLockedWalletUM(): WalletUM.Locked { + return WalletUM.Locked( + walletsBalanceUM = WalletBalanceUM.Loading( + id = walletId, + name = name, + deviceIcon = getWalletIconUseCase.invoke(userWallet = this) + .let { WalletIconUMConverter().convert(it) }, + ), + buttons = createWalletActions(userWallet = this), + type = when (this) { + is UserWallet.Cold -> WalletType.Cold + is UserWallet.Hot -> WalletType.Hot + }, + notifications = persistentListOf( + WalletNotificationUM.UnlockWallets( + onClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + ), + ), + ) + } + private fun createMultiWalletEnabledButtons(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() @@ -110,4 +151,28 @@ internal class InitializeWalletsTransformer( WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), ) } + + private fun createWalletActions(userWallet: UserWallet): PersistentList { + return buildList { + add( + WalletActionButtons.Buy( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + addIf( + condition = !userWallet.isSingleWallet(), + element = WalletActionButtons.Swap( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + }.toPersistentList() + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt index 74aaa23e95..b0bf9f08c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class OpenBottomSheetTransformer( userWalletId: UserWalletId, @@ -28,6 +29,10 @@ internal class OpenBottomSheetTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig() = TangemBottomSheetConfig( isShown = true, onDismissRequest = onDismissBottomSheet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index d6072f30f7..0c1b198d0c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -22,12 +23,14 @@ internal class ReinitializeNewWalletTransformer( private val newUserWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } @@ -41,6 +44,13 @@ internal class ReinitializeNewWalletTransformer( ), ) .toImmutableList(), + wallets2 = prevState.wallets2 + .filterNot { it.walletsBalanceUM.id == prevWalletId } + .plus( + element = walletLoadingStateFactory.create2( + userWallet = newUserWallet, + ), + ).toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 4459476877..6dab085776 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory /** @@ -17,12 +19,20 @@ internal class ReinitializeWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, + ) + } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletLoadingStateFactory.create2( + userWallet = userWallet, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt index 0ccb1652e4..ae06ec59bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class RemoveNFTCollectionsTransformer( userWalletId: UserWalletId, @@ -17,4 +18,13 @@ internal class RemoveNFTCollectionsTransformer( is WalletState.SingleCurrency.Locked, -> prevState } + + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletUM.Locked -> walletUM + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt index a87c58fcc2..af3f38b3a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt @@ -3,7 +3,9 @@ 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 com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import timber.log.Timber /** @@ -25,8 +27,16 @@ internal class RenameWalletsTransformer( } else { walletState } - } - .toImmutableList(), + }.toImmutableList(), + wallets2 = prevState.wallets2.map { walletUM -> + val renamedWallet = renamedWallets.firstOrNull { it.walletId == walletUM.walletsBalanceUM.id } + + if (renamedWallet != null) { + transform(prevState = walletUM, newName = renamedWallet.name) + } else { + walletUM + } + }.toPersistentList(), ) } @@ -46,4 +56,16 @@ internal class RenameWalletsTransformer( } } } + + private fun transform(prevState: WalletUM, newName: String): WalletUM { + return when (prevState) { + is WalletUM.Content -> { + prevState.copy(walletsBalanceUM = prevState.walletsBalanceUM.copySealed(name = newName)) + } + is WalletUM.Locked -> { + Timber.e("Impossible to rename wallet in locked state") + prevState + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index f414b696e9..f3dbd5dd9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -1,13 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -15,7 +16,7 @@ import timber.log.Timber internal class SetCryptoCurrencyActionsTransformer( private val tokenActionsState: TokenActionsState, private val userWallet: UserWallet, - private val portfolioId: PortfolioId, + private val accountId: AccountId, private val clickIntents: WalletClickIntents, ) : WalletStateTransformer(userWallet.walletId) { @@ -35,6 +36,10 @@ internal class SetCryptoCurrencyActionsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TokenActionsState.toManageButtons(): PersistentList { return states .filterIfS2C() @@ -46,7 +51,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onBuyClick( - userWalletId = portfolioId.userWalletId, + accountId = accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) @@ -59,7 +64,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onReceiveClick( - portfolioId.userWalletId, + accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, ) }, @@ -86,7 +91,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onSendClick( - userWalletId = portfolioId.userWalletId, + accountId = accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index d41cf131aa..9d0afb1eff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -56,6 +57,10 @@ internal class SetExpressStatusesTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet( expressState: ExpressTransactionStateUM?, ): TangemBottomSheetConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index 4d73f70ed2..caa9d7ec25 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.allLoadedCollectionsEmpty import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toPersistentList internal class SetNFTCollectionsTransformer( @@ -28,6 +29,19 @@ internal class SetNFTCollectionsTransformer( -> prevState } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + nftState = when { + nftCollections.allLoadedCollectionsEmpty() -> + WalletNFTItemUM.Empty(onItemClick) + else -> createContentNFTItemUM(onItemClick) + }, + ) + is WalletUM.Locked -> walletUM + } + } + private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content { val collectionsContent = nftCollections .map { it.content } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index b09dc47d03..5efac70c6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -6,10 +6,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter import timber.log.Timber +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SetPrimaryCurrencyTransformer( private val userWallet: UserWallet, private val status: CryptoCurrencyStatus, @@ -35,6 +37,10 @@ internal class SetPrimaryCurrencyTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // It will not be used + } + private fun WalletCardState.toLoadedSingleCurrencyState(): WalletCardState { return SingleWalletCardStateConverter(status.value, userWallet, appCurrency).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 1ad7d9cb18..504e4192e7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -33,6 +33,17 @@ internal class SetRefreshStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + pullToRefreshConfig = walletUM.pullToRefreshConfig.toUpdatedState(isRefreshing), + tokensListUM = walletUM.tokensListUM.toUpdatedState(), + buttons = walletUM.enableButtons(), + ) + is WalletUM.Locked -> walletUM + } + } + private fun PullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): PullToRefreshConfig { return copy(isRefreshing = isRefreshing) } @@ -49,6 +60,16 @@ internal class SetRefreshStateTransformer( } } + private fun WalletTokensListUM.toUpdatedState(): WalletTokensListUM { + return if (this is WalletTokensListUM.Content && organizeButtonUM != null) { + copy( + organizeButtonUM = organizeButtonUM.copy(isEnabled = !isRefreshing), + ) + } else { + this + } + } + private fun PersistentList.toUpdatedState(): PersistentList { val isButtonsEnabled = !isRefreshing diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 41d72e9aeb..0d7dc02991 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -1,15 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import androidx.compose.ui.text.SpanStyle import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -51,6 +52,22 @@ internal class SetTokenListErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(), + tokensListUM = WalletTokensListUM.Empty, + buttons = walletUM.disableButtons(), + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } + } + private fun WalletCardState.toLoadedState(): WalletCardState { return WalletCardState.Content( id = id, @@ -69,4 +86,28 @@ internal class SetTokenListErrorTransformer( isBalanceFlickering = false, ) } + + private fun WalletBalanceUM.toLoadedState(): WalletBalanceUM { + return WalletBalanceUM.Content( + id = id, + name = name, + deviceIcon = deviceIcon, + balanceInAppBar = BigDecimal.ZERO.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + balance = BigDecimal.ZERO.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { TangemTheme.typography2.headingRegular28.toSpanStyle() }, + ) + }, + isZeroBalance = true, + isBalanceFlickering = false, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 8a7a452474..57be4ec68c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -5,11 +5,12 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber import java.math.BigDecimal @@ -22,6 +23,7 @@ internal class SetTokenListTransformer( private val yieldSupplyApyMap: Map = emptyMap(), private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, + private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -45,6 +47,22 @@ internal class SetTokenListTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState2(), + tokensListUM = toLoadedState(), + buttons = walletUM.enableButtons(), + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } + } + private fun WalletCardState.toLoadedState(): WalletCardState { val fiatBalance = when (params) { is TokenConverterParams.Account -> params.accountList.totalFiatBalance @@ -57,6 +75,17 @@ internal class SetTokenListTransformer( ).convert(value = this) } + private fun WalletBalanceUM.toLoadedState2(): WalletBalanceUM { + val fiatBalance = when (params) { + is TokenConverterParams.Account -> params.accountList.totalFiatBalance + is TokenConverterParams.Wallet -> params.tokenList.totalFiatBalance + } + return MultiWalletBalanceUMTransformer( + fiatBalance = fiatBalance, + appCurrency = appCurrency, + ).transform(prevState = this) + } + private fun WalletTokensListState.toLoadedState(): WalletTokensListState { return TokenListStateConverter( params = params, @@ -68,4 +97,19 @@ internal class SetTokenListTransformer( shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } + + private fun toLoadedState(): WalletTokensListUM { + if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty + + return WalletTokensListUMConverter( + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldModuleApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountsModeEnabled, + expandedAccounts = params.expandedAccounts, + ).convert(value = params.accountList) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index f4fcbf57fc..9a2b1164f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetTxHistoryCountErrorTransformer( ) } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt index f89b8af13e..cba6ac874b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import timber.log.Timber @@ -33,6 +34,10 @@ internal class SetTxHistoryCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toLoadingState(): TxHistoryState { return if (this is TxHistoryState.Content) { Timber.d("Load transactions history: $transactionsCount") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt index d16f26c066..b8af717c52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class SetTxHistoryItemsErrorTransformer( @@ -27,6 +28,10 @@ internal class SetTxHistoryItemsErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createErrorState(): TxHistoryState.Error = when (error) { is TxHistoryListError.DataError -> { TxHistoryState.Error( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt index d94f18179b..289daa4514 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemFlowConverter import kotlinx.coroutines.flow.Flow import timber.log.Timber @@ -32,6 +33,10 @@ internal class SetTxHistoryItemsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toContentState(): TxHistoryState { val converter = TxHistoryItemFlowConverter( currentState = this, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index 45b0aabd50..fde5efd497 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -2,13 +2,18 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import timber.log.Timber internal class SetWarningsTransformer( userWalletId: UserWalletId, private val warnings: ImmutableList, + private val notifications: ImmutableList = persistentListOf(), + private val notificationsCarousel: ImmutableList = persistentListOf(), ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -23,4 +28,17 @@ internal class SetWarningsTransformer( } } } + + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + notifications = notifications, + notificationsCarousel = notificationsCarousel, + ) + is WalletUM.Locked -> { + Timber.w("Impossible to update notifications for locked wallet") + walletUM + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt index b8a735a4f4..cd344f9f4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayExposedDeviceTransformer( userWalletId: UserWalletId, @@ -14,4 +15,8 @@ internal class TangemPayExposedDeviceTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt index 171cb8aaed..bbb5035c07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHiddenStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHiddenStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt index 9e4e6bafd7..8c1f641c38 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHideOnboardingStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHideOnboardingStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt index f731cb119d..6404796e7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -12,4 +13,8 @@ internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : Wa prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt index 15b2ad4dee..4659e5f486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayOnboardingBannerStateTransformer( userWalletId: UserWalletId, @@ -22,4 +23,8 @@ internal class TangemPayOnboardingBannerStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index 98a3ee0caa..e2c4d1a6c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshNeededStateTransformer( userWalletId: UserWalletId, @@ -32,4 +33,8 @@ internal class TangemPayRefreshNeededStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index cd5885b406..b37a6f916f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, @@ -21,4 +22,8 @@ internal class TangemPayRefreshShowProgressTransformer( ), ) } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt index 5cdc1926c9..5b2a7765a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayUnavailableStateTransformer( userWalletId: UserWalletId, @@ -20,4 +21,8 @@ internal class TangemPayUnavailableStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index c995e3dd44..2839523857 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.model.CustomerInfo.CardInfo @@ -16,8 +17,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import java.util.Currency /** @@ -42,6 +42,10 @@ internal class TangemPayUpdateInfoStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createInitialState(): TangemPayState { val cardInfo = value.info.cardInfo val productInstance = value.info.productInstance @@ -50,7 +54,7 @@ internal class TangemPayUpdateInfoStateTransformer( // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) - value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() -> + value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() -> createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId) cardInfo != null && productInstance != null -> getCardInfoState(customerId, cardInfo, productInstance) @@ -74,7 +78,6 @@ internal class TangemPayUpdateInfoStateTransformer( cardId = productInstance.cardId, isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, - customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits, chainId = POLYGON_CHAIN_ID, ), @@ -89,25 +92,24 @@ internal class TangemPayUpdateInfoStateTransformer( } } - private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState = - Progress( - title = TextReference.Res(R.string.tangempay_payment_account), - description = when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) - else -> TextReference.Res(R.string.tangempay_kyc_in_progress) - }, - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = { - when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( - userWalletId = userWalletId, - customerId = customerId, - ) - else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) - } - }, - ) + private fun createKycInProgressState(kycStatus: KycStatus, customerId: String): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_payment_account), + description = when (kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = { + when (kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = userWalletId, + customerId = customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) + } + }, + ) private fun createIssueProgressState(): TangemPayState = Progress( title = TextReference.Res(R.string.tangempay_payment_account), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt index fd8229e5e8..03ffab9a92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt @@ -1,16 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.tokenlist.TokenList sealed interface TokenConverterParams { + /** Wallet mode; list of tokens for main account */ data class Wallet( - val portfolioId: PortfolioId, + val accountId: AccountId, val tokenList: TokenList, ) : TokenConverterParams + /** Account mode; list of accounts */ data class Account( val accountList: AccountStatusList, val expandedAccounts: Set, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt deleted file mode 100644 index bb5c7ca1cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.transformers - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import kotlin.reflect.KClass - -internal abstract class TypedWalletStateTransformer( - userWalletId: UserWalletId, - protected val targetStateClass: KClass, -) : WalletStateTransformer(userWalletId) { - - abstract fun transformTyped(prevState: S): WalletState - - @Suppress("UNCHECKED_CAST") - final override fun transform(prevState: WalletState): WalletState { - return if (prevState::class == targetStateClass) { - transformTyped(prevState as S) - } 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/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 4e87f77c40..fcef9841df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -2,24 +2,29 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import timber.log.Timber internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } @@ -31,6 +36,18 @@ internal class UnlockWalletTransformer( if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet) } .toImmutableList(), + wallets2 = prevState.wallets2 + .map { walletUM -> + val unlockedWallet = getUnlockedWallet(walletUM.walletsBalanceUM.id) + if (unlockedWallet == null) { + walletUM + } else { + createLoadingState2( + walletUM = walletUM, + unlockedWallet = unlockedWallet, + ) + } + }.toPersistentList(), ) } @@ -53,4 +70,16 @@ internal class UnlockWalletTransformer( } } } + + private fun createLoadingState2(walletUM: WalletUM, unlockedWallet: UserWallet): WalletUM { + return when (walletUM) { + is WalletUM.Locked -> walletLoadingStateFactory.create2( + userWallet = unlockedWallet, + ) + is WalletUM.Content -> { + Timber.e("Impossible to unlock wallet with not locked state") + walletUM + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt index b723034b32..b6ad62458f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.showSwapBadge internal class UpdateMultiWalletActionButtonBadgeTransformer( @@ -16,4 +17,8 @@ internal class UpdateMultiWalletActionButtonBadgeTransformer( else -> prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 3d29e45a4a..72c403ae19 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class UpdateWalletCardsCountTransformer( @@ -30,6 +31,10 @@ internal class UpdateWalletCardsCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toUpdatedState(): WalletCardState { return when (this) { is WalletCardState.Content -> copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index be06139bf9..5aa3c34900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList internal abstract class WalletStateTransformer( @@ -11,6 +12,8 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState + abstract fun transform(walletUM: WalletUM): WalletUM + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets @@ -18,6 +21,11 @@ internal abstract class WalletStateTransformer( if (state.walletCardState.id == userWalletId) transform(state) else state } .toImmutableList(), + wallets2 = prevState.wallets2 + .map { walletUM -> + if (walletUM.walletsBalanceUM.id == userWalletId) transform(walletUM) else walletUM + } + .toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt new file mode 100644 index 0000000000..25f9f9df7b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +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.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.staking.StakingBalance +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.feature.wallet.presentation.wallet.state.transformers.converter.EarnApyConverter.EarnApyInfo +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnApyConverter( + val yieldModuleApyMap: Map, + val stakingApyMap: Map, +) : Converter { + + override fun convert(value: CryptoCurrencyStatus): EarnApyInfo? { + val token = value.currency as? CryptoCurrency.Token + if (token != null && yieldModuleApyMap.isNotEmpty()) { + val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { apy -> + apy.key.equals( + other = token.yieldSupplyKey(), + ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), + ) + }?.value + if (yieldSupplyApy != null) { + val isActive = value.value.yieldSupplyStatus?.isActive == false + return EarnApyInfo( + text = resourceReference( + R.string.yield_module_earn_badge, + wrappedList(yieldSupplyApy), + ), + isActive = isActive, + apy = yieldSupplyApy.toString(), + source = TokenItemStateConverter.ApySource.YIELD_SUPPLY, + ) + } + } + + if (stakingApyMap.isNotEmpty()) { + val stakingInfo = findStakingRate( + currencyStatus = value, + stakingApyMap = stakingApyMap, + ) + val rewardTypeRes = when (stakingInfo.rewardType) { + RewardType.APR -> R.string.staking_apr_earn_badge + RewardType.UNKNOWN, + RewardType.APY, + null, + -> R.string.yield_module_earn_badge + } + if (stakingInfo.rate != null) { + val apyString = stakingInfo.rate.format { percent(withPercentSign = false) } + return EarnApyInfo( + text = resourceReference( + rewardTypeRes, + wrappedList(apyString), + ), + isActive = stakingInfo.isActive, + apy = apyString, + source = TokenItemStateConverter.ApySource.STAKING, + ) + } + } + + return null + } + + private fun findStakingRate( + currencyStatus: CryptoCurrencyStatus, + stakingApyMap: Map, + ): StakingLocalInfo { + val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available + ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) + + val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data + val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit + val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool + + val rateInfo = when (val stakingOptions = stakingAvailability.option) { + is StakingOption.P2PEthPool -> { + RewardInfo( + rate = stakingOptions.apy, + type = RewardType.APY, + ) + } + is StakingOption.StakeKit -> if (stakeKitBalance != null) { + val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } + stakeKitBalance.balance.items + .mapNotNull { it.validatorAddress } + .firstNotNullOfOrNull { address -> + validatorsByAddress[address]?.rewardInfo + } ?: stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } else { + stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } + } + + return StakingLocalInfo( + rate = rateInfo?.rate, + isActive = stakeKitBalance != null || p2pEthPoolBalance != null, + rewardType = rateInfo?.type, + ) + } + + data class StakingLocalInfo( + val rate: BigDecimal?, + val isActive: Boolean, + val rewardType: RewardType?, + ) + + data class EarnApyInfo( + val text: TextReference?, + val isActive: Boolean, + val apy: String?, + val source: TokenItemStateConverter.ApySource, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt new file mode 100644 index 0000000000..d3a37dbb31 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.utils.extensions.isZero +import com.tangem.utils.transformer.Transformer + +internal class MultiWalletBalanceUMTransformer( + private val fiatBalance: TotalFiatBalance, + private val appCurrency: AppCurrency, +) : Transformer { + + override fun transform(prevState: WalletBalanceUM): WalletBalanceUM { + return when (fiatBalance) { + is TotalFiatBalance.Loading -> prevState.toLoadingState() + is TotalFiatBalance.Failed -> prevState.toErrorState() + is TotalFiatBalance.Loaded -> prevState.toWalletCardState(fiatBalance) + } + } + + private fun WalletBalanceUM.toLoadingState(): WalletBalanceUM { + return WalletBalanceUM.Loading( + id = id, + name = name, + deviceIcon = deviceIcon, + ) + } + + private fun WalletBalanceUM.toErrorState(): WalletBalanceUM { + return WalletBalanceUM.Error( + id = id, + name = name, + deviceIcon = deviceIcon, + ) + } + + private fun WalletBalanceUM.toWalletCardState(fiatBalance: TotalFiatBalance.Loaded): WalletBalanceUM { + return WalletBalanceUM.Content( + id = id, + name = name, + deviceIcon = deviceIcon, + balanceInAppBar = fiatBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + balance = fiatBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { + TangemTheme.typography2.headingRegular28.toSpanStyle() + }, + ) + }, + isZeroBalance = fiatBalance.amount.isZero(), + isBalanceFlickering = fiatBalance.source == StatusSource.CACHE, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index ad35b058ea..4044cedde1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -2,15 +2,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWalletWithToken import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList @@ -18,12 +18,11 @@ import kotlinx.collections.immutable.toImmutableList internal class MultiWalletCurrencyActionsConverter( private val userWallet: UserWallet, + private val accountId: AccountId, private val clickIntents: WalletCurrencyActionsClickIntents, -) : Converter> { +) : Converter> { - private val userWalletId: UserWalletId = userWallet.walletId - - override fun convert(value: TokenActionsState): ImmutableList { + override fun convert(value: TokenActionsState): ImmutableList { return value.states .filterIfSingleWithToken() .mapNotNull { @@ -33,9 +32,7 @@ internal class MultiWalletCurrencyActionsConverter( } private fun List.filterIfSingleWithToken(): List { - return if (userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - ) { + return if (userWallet.isSingleWalletWithToken()) { filter { it !is TokenActionsState.ActionState.HideToken } } else { this @@ -46,7 +43,7 @@ internal class MultiWalletCurrencyActionsConverter( private fun mapTokenActionState( actionsState: TokenActionsState.ActionState, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): TokenActionButtonConfig? { + ): TokenActionButtonUM? { if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) { return null } @@ -58,17 +55,17 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Buy -> { title = resourceReference(R.string.common_buy) icon = R.drawable.ic_plus_24 - action = { clickIntents.onBuyClick(userWalletId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onBuyClick(accountId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Receive -> { title = resourceReference(R.string.common_receive) icon = R.drawable.ic_arrow_down_24 - action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onReceiveClick(accountId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.option) } + action = { clickIntents.onStakeClick(accountId, cryptoCurrencyStatus, actionsState.option) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) @@ -78,7 +75,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Send -> { title = resourceReference(R.string.common_send) icon = R.drawable.ic_arrow_up_24 - action = { clickIntents.onSendClick(userWalletId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onSendClick(accountId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) @@ -86,7 +83,7 @@ internal class MultiWalletCurrencyActionsConverter( action = { clickIntents.onSwapClick( cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = userWalletId, + accountId = accountId, unavailabilityReason = noneReason, ) } @@ -94,12 +91,12 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) icon = R.drawable.ic_copy_24 - action = { clickIntents.onCopyAddressClick(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onCopyAddressClick(accountId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.HideToken -> { title = resourceReference(R.string.token_details_hide_token) icon = R.drawable.ic_hide_24 - action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onHideTokensClick(accountId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Analytics -> { title = resourceReference(R.string.common_analytics) @@ -113,12 +110,12 @@ internal class MultiWalletCurrencyActionsConverter( } } - return TokenActionButtonConfig( + return TokenActionButtonUM( text = title, iconResId = icon, onClick = action, isWarning = actionsState is TokenActionsState.ActionState.HideToken, - enabled = actionsState.unavailabilityReason == noneReason, + isEnabled = actionsState.unavailabilityReason == noneReason, ) } } \ 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 1847ff842f..1ad54a1a82 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 @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference @@ -9,7 +11,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId @@ -49,35 +50,39 @@ internal class TokenListStateConverter( shouldShowMainPromo, ) - private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = + private val onTokenClick: (accountId: AccountId, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemClick(accountId, currencyStatus) } - private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = + private val onTokenLongClick: (accountId: AccountId, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemLongClick(accountId, currencyStatus) } - private val onApyLabelClick: - (currencyStatus: CryptoCurrencyStatus, apySource: TokenItemStateConverter.ApySource, apy: String) -> Unit = - { currencyStatus, apySource, apy -> + private val onApyLabelClick: ( + currencyStatus: CryptoCurrencyStatus, + accountId: AccountId, + apySource: TokenItemStateConverter.ApySource, + apy: String, + ) -> Unit = + { currencyStatus, accountId, apySource, apy -> clickIntents.onApyLabelClick( - userWalletId = selectedWallet.walletId, + accountId = accountId, currencyStatus = currencyStatus, apySource = apySource, apy = apy, ) } - private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( + private fun tokenStatusConverter(accountId: AccountId) = TokenItemStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, promoCryptoCurrencyStatus = yieldSupplyPromoBannerConverter.convert(params), stakingApyMap = stakingAvailabilityMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, - onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, + onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, accountId, apySource, apy) }, onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, onYieldPromoShown = clickIntents::onYieldPromoShown, onYieldPromoClicked = clickIntents::onYieldPromoClicked, @@ -87,7 +92,7 @@ internal class TokenListStateConverter( return when (params) { is TokenConverterParams.Account -> convertAccountList(params) is TokenConverterParams.Wallet -> convertTokenList( - tokenConverter = tokenStatusConverter((params.portfolioId as? PortfolioId.Account)?.accountId), + tokenConverter = tokenStatusConverter(params.accountId), tokenList = params.tokenList, ) } @@ -136,12 +141,17 @@ internal class TokenListStateConverter( is WalletTokensListState.ContentState.Locked -> tokensListState.items is WalletTokensListState.Empty -> listOf() } - return TokensListItemUM.Portfolio( + val onEmptyAction = PortfolioItemContentUM.Empty.Action( + text = resourceReference(id = R.string.onboarding_add_tokens), + onClick = { clickIntents.onManageTokensClick(account.accountId) }, + ) + return TokensListPortfolioItemConverter( tokenItemUM = accountItem, isExpanded = isExtend, isCollapsable = true, tokens = items.filterIsInstance().toPersistentList(), - ) + onEmptyAction = onEmptyAction, + ).convert(Unit) } val accountItems = accountList.accountStatuses diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt new file mode 100644 index 0000000000..75a56e8010 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt @@ -0,0 +1,130 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.ui.account.AccountIconItemStateConverter +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.account.AccountIconSize +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.pluralReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledResourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency +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.quote.PriceChange +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class WalletTokenAccountItemConverter( + private val appCurrency: AppCurrency, + private val expandedAccounts: Set, + private val onAccountCollapseClick: (account: Account) -> Unit, + private val onAccountExpandClick: (account: Account) -> Unit, +) : Converter { + override fun convert(value: AccountStatus.CryptoPortfolio): TangemTokenRowUM { + val account = value.account + val isExpanded = expandedAccounts.contains(account.accountId) + + return TangemTokenRowUM.Content( + id = account.accountId.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account), + ), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = account.accountName.toUM().value, + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = pluralReference( + R.plurals.common_tokens_count, + count = account.tokensCount, + formatArgs = wrappedList(account.tokensCount), + ), + ), + topEndContentUM = getTopEndContent(value.tokenList.totalFiatBalance), + bottomEndContentUM = getBottomEndContent( + value.tokenList.totalFiatBalance, + value.priceChangeLce.getOrNull(), + ), + onItemClick = { + if (isExpanded) { + onAccountCollapseClick(account) + } else { + onAccountExpandClick(account) + } + }, + onItemLongClick = null, + ) + } + + private fun getTopEndContent(accountBalance: TotalFiatBalance): TangemTokenRowUM.EndContentUM { + return when (accountBalance) { + TotalFiatBalance.Failed -> TangemTokenRowUM.EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) + is TotalFiatBalance.Loaded -> TangemTokenRowUM.EndContentUM.Content( + text = accountBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ) + TotalFiatBalance.Loading -> TangemTokenRowUM.EndContentUM.Loading + } + } + + private fun getBottomEndContent( + accountBalance: TotalFiatBalance, + priceChange: PriceChange?, + ): TangemTokenRowUM.EndContentUM { + return when (accountBalance) { + TotalFiatBalance.Failed -> TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is TotalFiatBalance.Loaded -> if (priceChange != null) { + val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value) + + TangemTokenRowUM.EndContentUM.Content( + text = stringReference( + priceChange.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = priceChangeType, + valueInPercent = priceChange.value.format { percent() }, + ), + ) + } else { + TangemTokenRowUM.EndContentUM.Empty + } + TotalFiatBalance.Loading -> TangemTokenRowUM.EndContentUM.Loading + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt new file mode 100644 index 0000000000..ed2a68e455 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt @@ -0,0 +1,300 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledResourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.feature.wallet.child.wallet.model.intents.WalletContentClickIntents +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +internal class WalletTokenCurrencyItemConverter( + private val appCurrency: AppCurrency, + private val accountId: AccountId, + private val shouldShowPromo: Boolean, + private val yieldModuleApyMap: Map, + private val clickIntents: WalletContentClickIntents, + stakingAvailabilityMap: Map, +) : Converter { + + private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + private val earnApyConverter = EarnApyConverter( + yieldModuleApyMap = yieldModuleApyMap, + stakingApyMap = stakingAvailabilityMap, + ) + + override fun convert(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM { + val earnApyInfo = earnApyConverter.convert(currencyStatus) + + return TangemTokenRowUM.Content( + id = currencyStatus.currency.id.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = currencyToIconStateConverter.convert(currencyStatus), + ), + titleUM = toCurrencyRowTitle(currencyStatus, earnApyInfo), + subtitleUM = toCurrencyRowSubtitle(currencyStatus), + topEndContentUM = toCurrencyRowTopEnd(currencyStatus), + bottomEndContentUM = toCurrencyRowBottomEnd(currencyStatus), + promoBannerUM = toPromoBannerUM( + accountId, + currencyStatus, + earnApyInfo.takeIf { shouldShowPromo }, + ), + onItemClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + -> null + else -> { + { + clickIntents.onTokenItemClick(accountId, currencyStatus) + } + } + }, + onItemLongClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading -> null + else -> { + { + clickIntents.onTokenItemLongClick(accountId, currencyStatus) + } + } + }, + ) + } + + private fun toCurrencyRowTitle( + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.TitleUM = when (val value = currencyStatus.value) { + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + ) + } + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + hasPending = value.hasCurrentNetworkTransactions, + badge = if (earnApyInfo != null && earnApyInfo.text != null) { + TangemBadgeUM( + type = TangemBadgeType.Solid, + color = when { + earnApyInfo.isActive -> TangemBadgeColor.Blue + else -> TangemBadgeColor.Gray + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X4, + text = earnApyInfo.text, + onClick = if (earnApyInfo.apy != null) { + { + clickIntents.onApyLabelClick( + accountId = accountId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + } + } else { + null + }, + ) + } else { + null + }, + ) + } + } + + private fun toCurrencyRowSubtitle(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.SubtitleUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.SubtitleUM.Loading + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> TangemTokenRowUM.SubtitleUM.Content( + text = stringReference( + currencyStatus.value.fiatRate.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = PriceChangeType.fromBigDecimal(currencyStatus.value.priceChange.orZero()), + valueInPercent = currencyStatus.value.priceChange.format { percent() }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> TangemTokenRowUM.SubtitleUM.Empty + } + } + + private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.EndContentUM { + val yieldSupply = currencyStatus.value.yieldSupplyStatus + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TangemTokenRowUM.EndContentUM.Content( + text = currencyStatus.getTotalFiatAmount().formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + isFlickering = currencyStatus.value.isFlickering(), + startIcons = buildList { + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + condition = yieldSupply?.isActive == true && !yieldSupply.isAllowedToSpend, + ) + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.tertiary }, + ), + condition = currencyStatus.value.sources.total == StatusSource.ONLY_CACHE, + ) + }.toImmutableList(), + ) + } + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> TangemTokenRowUM.EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) + is CryptoCurrencyStatus.Unreachable -> TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> TangemTokenRowUM.EndContentUM.Empty + } + } + + private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.EndContentUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> TangemTokenRowUM.EndContentUM.Content( + text = stringReference( + currencyStatus.getTotalCryptoAmount().format { + crypto(cryptoCurrency = currencyStatus.currency) + }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_no_address, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.Unreachable -> TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> TangemTokenRowUM.EndContentUM.Empty + } + } + + private fun toPromoBannerUM( + accountId: AccountId, + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.PromoBannerUM { + val currency = currencyStatus.currency + val isTokenCurrency = currency is CryptoCurrency.Token + val isCurrencyStatusLoaded = currencyStatus.value is CryptoCurrencyStatus.Loaded + val isApyInfoNotNull = earnApyInfo != null && earnApyInfo.apy != null + + if (!isTokenCurrency || !isCurrencyStatusLoaded || !isApyInfoNotNull) { + return TangemTokenRowUM.PromoBannerUM.Empty + } + + return TangemTokenRowUM.PromoBannerUM.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(earnApyInfo.apy), + ), + onPromoBannerClick = { + clickIntents.onYieldPromoClicked(currency) + clickIntents.onApyLabelClick( + accountId = accountId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + }, + onCloseClick = clickIntents::onYieldPromoCloseClick, + onPromoShown = { + clickIntents.onYieldPromoShown(currency) + }, + ) + } + + private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt new file mode 100644 index 0000000000..005d2bb298 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -0,0 +1,177 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWalletWithToken +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class WalletTokensListUMConverter( + private val appCurrency: AppCurrency, + private val selectedWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val yieldModuleApyMap: Map, + private val isAccountsModeEnabled: Boolean, + private val expandedAccounts: Set, + private val stakingAvailabilityMap: Map, + shouldShowMainPromo: Boolean, +) : Converter { + + private val accountRowConverter by lazy(LazyThreadSafetyMode.NONE) { + WalletTokenAccountItemConverter( + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + onAccountCollapseClick = clickIntents::onAccountCollapseClick, + onAccountExpandClick = clickIntents::onAccountExpandClick, + ) + } + + private val yieldSupplyPromoBannerConverter by lazy(LazyThreadSafetyMode.NONE) { + YieldSupplyPromoBannerConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + } + + private fun currencyRowConverter( + accountId: AccountId, + shouldShowPromo: Boolean, + ): WalletTokenCurrencyItemConverter { + return WalletTokenCurrencyItemConverter( + appCurrency = appCurrency, + accountId = accountId, + shouldShowPromo = shouldShowPromo, + yieldModuleApyMap = yieldModuleApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + clickIntents = clickIntents, + ) + } + + override fun convert(value: AccountStatusList): WalletTokensListUM { + val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) + return if (value.accountStatuses.isEmpty()) { + WalletTokensListUM.Empty + } else { + val isCollapsable = value.accountStatuses.count { + it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 + } > 1 + + val tokenListUM = value.accountStatuses + .filterIsInstance() + .asSequence() + .flatMap { accountStatus -> + if (isAccountsModeEnabled) { + val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) + sequenceOf( + TokensListItemUM2.Portfolio( + tokenRowUM = accountRowConverter.convert(accountStatus), + isExpanded = isExpanded || !isCollapsable, + isCollapsable = isCollapsable, + tokenList = getTokenListItems( + accountStatus, + promoCryptoCurrency, + ).toPersistentList(), + ), + ) + } else { + getTokenListItems(accountStatus, promoCryptoCurrency) + } + }.toPersistentList() + + WalletTokensListUM.Content( + tokenList = tokenListUM, + organizeButtonUM = getOrganizeButtonUM(value), + ) + } + } + + private fun getTokenListItems( + accountStatus: AccountStatus.CryptoPortfolio, + promoCryptoCurrency: CryptoCurrencyStatus?, + ): Sequence { + return when (val tokenList = accountStatus.tokenList) { + TokenList.Empty -> emptySequence() + is TokenList.GroupedByNetwork -> { + tokenList.groups.asSequence().flatMap { (network, currencies) -> + buildList { + add( + TokensListItemUM2.GroupTitle( + tokenRowUM = toGroupRow(network), + ), + ) + addAll( + currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = currencyRowConverter( + accountStatus.accountId, + shouldShowPromo, + ).convert(currencyStatus), + ) + }.toList(), + ) + } + } + } + is TokenList.Ungrouped -> { + tokenList.currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = currencyRowConverter( + accountStatus.accountId, + shouldShowPromo, + ).convert(currencyStatus), + ) + } + } + } + } + + private fun toGroupRow(network: Network): TangemHeaderRowUM { + return TangemHeaderRowUM( + id = network.hashCode().toString(), + title = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(network.name), + ), + ) + } + + private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { + return if (accountList.flattenCurrencies().size > 1 && !selectedWallet.isSingleWalletWithToken()) { + TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + type = TangemButtonType.PrimaryInverse, + iconRes = R.drawable.ic_filter_default_24, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index 26c9df46a3..a94d6b5b78 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey @@ -28,6 +29,35 @@ internal class YieldSupplyPromoBannerConverter( if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + val tokenKey = "${token.network.rawId}_${token.contractAddress}" + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.first + } + + fun convert2(value: AccountStatusList): CryptoCurrencyStatus? { + if (!shouldShowMainPromo) return null + + val currencies = value.flattenCurrencies().filter { status -> + status.value is CryptoCurrencyStatus.Loaded + } + + val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token } + + if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() .mapNotNull { status -> val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt index b89808ca72..f5506e2b29 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList @@ -13,6 +15,14 @@ internal fun WalletState.MultiCurrency.Content.disableButtons(): PersistentList< return changeAvailability(enabled = false) } +internal fun WalletUM.Content.enableButtons(): PersistentList { + return buttons.map { it.copy(isEnabled = true) }.toPersistentList() +} + +internal fun WalletUM.Content.disableButtons(): PersistentList { + return buttons.map { it.copy(isEnabled = false) }.toPersistentList() +} + private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolean): PersistentList { return buttons .map { action -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt index 14685c549d..8e3ebc996c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt @@ -17,4 +17,12 @@ internal inline fun UserWallet.createStateByWalletType( private fun UserWallet.Cold.isWalletWithTokens(): Boolean { return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken() +} + +internal fun UserWallet.isSingleWallet(): Boolean { + return this is UserWallet.Cold && scanResponse.cardTypesResolver.isSingleWallet() +} + +internal fun UserWallet.isSingleWalletWithToken(): Boolean { + return this is UserWallet.Cold && scanResponse.cardTypesResolver.isSingleWalletWithToken() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index b38da828a2..06d9f69510 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 @@ -1,22 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.WALLET_TYPE 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.ds.button.TangemButtonUM 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.domain.wallets.usecase.GetWalletIconUseCase 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 com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow /** @@ -27,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) { fun create(userWallet: UserWallet): WalletState { @@ -43,6 +49,28 @@ internal class WalletLoadingStateFactory( } } + fun create2(userWallet: UserWallet): WalletUM { + return WalletUM.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletsBalanceUM = WalletBalanceUM.Loading( + id = userWallet.walletId, + name = userWallet.name, + deviceIcon = getWalletIconUseCase.invoke(userWallet = userWallet) + .let { WalletIconUMConverter().convert(it) }, + ), + buttons = createWalletActions(userWallet), + notifications = persistentListOf(), + notificationsCarousel = persistentListOf(), + tokensListUM = WalletTokensListUM.Loading, + nftState = WalletNFTItemUM.Hidden, + type = when (userWallet) { + is UserWallet.Cold -> WalletType.Cold + is UserWallet.Hot -> WalletType.Hot + }, + tangemPayState = TangemPayState.Empty, + ) + } + private fun createLoadingHotWalletContent(userWallet: UserWallet.Hot): WalletState.MultiCurrency.Content { return WalletState.MultiCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), @@ -52,7 +80,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Hot, + type = WalletType.Hot, tangemPayState = TangemPayState.Empty, ) } @@ -66,7 +94,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Cold, + type = WalletType.Cold, tangemPayState = TangemPayState.Empty, ) } @@ -144,6 +172,39 @@ internal class WalletLoadingStateFactory( ) } + private fun createWalletActions(userWallet: UserWallet): PersistentList { + return buildList { + add( + WalletActionButtons.Buy( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletBuyClick( + userWalletId = userWallet.walletId, + screenType = WALLET_TYPE, + ) + }, + ).buttonUM, + ) + addIf( + condition = !userWallet.isSingleWallet(), + element = WalletActionButtons.Swap( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + }.toPersistentList() + } + private fun createDimmedButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index cf83e0d256..2d7ba38899 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -37,6 +38,7 @@ internal class AccountListSubscriber @AssistedInject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, + private val designFeatureToggles: DesignFeatureToggles, ) : BasicAccountListSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( @@ -51,15 +53,27 @@ internal class AccountListSubscriber @AssistedInject constructor( accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap, -> - updateState( - accountList = accountList, - appCurrency = appCurrency, - expandedAccounts = expandedAccounts, - isAccountMode = isAccountMode, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ) + if (designFeatureToggles.isRedesignEnabled) { + updateState2( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } else { + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } } private fun stakingAvailabilityFlow(): Flow> = getAccountStatusListFlow() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index f4b38a9290..8bff35ab6f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -6,7 +6,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList @@ -66,7 +65,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { singleAccountTransform( maybeTokenList = maybeTokenList, appCurrency = appCurrency, - portfolioId = PortfolioId(mainAccount.accountId), + accountId = mainAccount.accountId, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, @@ -85,10 +84,33 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } } + protected fun updateState2( + accountList: AccountStatusList, + appCurrency: AppCurrency, + expandedAccounts: Set, + isAccountMode: Boolean, + yieldSupplyApyMap: Map = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), + shouldShowMainPromo: Boolean = false, + ) { + stateController.update( + SetTokenListTransformer( + params = TokenConverterParams.Account(accountList, expandedAccounts), + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountMode, + ), + ) + } + private fun singleAccountTransform( maybeTokenList: Lce, appCurrency: AppCurrency, - portfolioId: PortfolioId, + accountId: AccountId, yieldSupplyApyMap: Map = emptyMap(), stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, @@ -117,7 +139,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ) updateContent( - params = TokenConverterParams.Wallet(portfolioId, tokenList), + params = TokenConverterParams.Wallet(accountId, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, @@ -141,6 +163,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt index a89f459bdc..6ebcbb2878 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.mapNotNull * [REDACTED_AUTHOR] */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal abstract class BasicSingleWalletSubscriber : BasicWalletSubscriber() { /** Account ID for the main crypto portfolio of the user wallet */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt deleted file mode 100644 index 3fbfb3592d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.getOrElse -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.TotalFiatBalance -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.staking.model.StakingAvailability -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -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 -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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.state.transformers.SetTokenListErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import java.math.BigDecimal - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal abstract class BasicTokenListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : WalletSubscriber() { - - private val sendAnalyticsJobHolder = JobHolder() - private val onTokenListReceivedJobHolder = JobHolder() - - protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow - - protected abstract suspend fun onTokenListReceived(maybeTokenList: Lce) - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = tokenListFlow(coroutineScope) - .onEach { maybeTokenList -> - coroutineScope.launch { - sendTokenListAnalytics( - flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(), - totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance, - ) - }.saveIn(sendAnalyticsJobHolder) - } - .distinctUntilChanged() - .onEach { maybeTokenList -> - coroutineScope.launch { - onTokenListReceived(maybeTokenList) - }.saveIn(onTokenListReceivedJobHolder) - }, - flow2 = appCurrencyFlow(), - flow3 = yieldSupplyApyFlow(), - flow4 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo -> - val tokenList = maybeTokenList.getOrElse( - ifLoading = { maybeContent -> - val isRefreshing = stateHolder.getWalletState(userWallet.walletId) - ?.pullToRefreshConfig - ?.isRefreshing == true - - maybeContent - ?.takeIf { !isRefreshing } - ?: return@combine - }, - ifError = { e -> - Timber.e("Failed to load token list: $e") - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = e, - appCurrency = appCurrency, - ), - ) - return@combine - }, - ) - - updateContent( - params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList), - appCurrency = appCurrency, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( - userWalletId = userWallet.walletId, - cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), - ), - shouldShowMainPromo = shouldShowMainPromo, - ) - - walletWithFundsChecker.check(tokenList) - }, - ) - } - - private suspend fun sendTokenListAnalytics( - flattenCurrencies: List?, - totalFiatBalance: TotalFiatBalance?, - ) { - val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) - - tokenListAnalyticsSender.send( - displayedUiState = displayedState, - userWallet = userWallet, - flattenCurrencies = flattenCurrencies ?: return, - totalFiatBalance = totalFiatBalance ?: return, - ) - } - - private fun updateContent( - params: TokenConverterParams, - appCurrency: AppCurrency, - yieldSupplyApyMap: Map, - stakingAvailabilityMap: Map, - shouldShowMainPromo: Boolean, - ) { - stateHolder.update( - SetTokenListTransformer( - params = params, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ), - ) - } - - private fun appCurrencyFlow(): Flow = getSelectedAppCurrencyUseCase() - .map { - it.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } - } - .distinctUntilChanged() - - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() - .distinctUntilChanged() - - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() - .distinctUntilChanged() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt index 426cac1ace..8ef8a70601 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt @@ -1,28 +1,37 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionButtonBadgeTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -internal class MultiWalletActionButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class MultiWalletActionButtonsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getStoryContentUseCase: GetStoryContentUseCase, ) : WalletSubscriber() { + override fun create(coroutineScope: CoroutineScope): Flow<*> = getStoryContentUseCase( id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, ).map { maybeSwapStories -> val isSwapStoriesNotNull = maybeSwapStories.getOrNull() != null - stateHolder.update( + stateController.update( UpdateMultiWalletActionButtonBadgeTransformer( userWalletId = userWallet.walletId, showSwapBadge = isSwapStoriesNotNull, ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletActionButtonsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt deleted file mode 100644 index c3159ba551..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError -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 -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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 kotlinx.coroutines.CoroutineScope - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal class MultiWalletTokenListSubscriber( - private val userWallet: UserWallet, - private val tokenListStore: MultiWalletTokenListStore, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - return tokenListStore.getOrThrow(userWallet.walletId) - } - - override suspend fun onTokenListReceived(maybeTokenList: Lce) { - updateSortingIfNeeded(maybeTokenList) - } - - private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { - val tokenList = getTokenList(maybeTokenList) ?: return - - applyTokenListSortingUseCase( - userWalletId = userWallet.walletId, - sortedTokensIds = getCurrenciesIds(tokenList), - isGroupedByNetwork = tokenList is TokenList.GroupedByNetwork, - isSortedByBalance = tokenList.sortedBy == TokensSortType.BALANCE, - ) - } - - private fun getTokenList(lce: Lce<*, TokenList>): TokenList? { - val tokenList = lce.getOrNull(isPartialContentAccepted = false) - ?: return null - - return tokenList.takeIf { - tokenList.totalFiatBalance is TotalFiatBalance.Loaded && - tokenList.sortedBy == TokensSortType.BALANCE - } - } - - private fun getCurrenciesIds(tokenList: TokenList): List { - return tokenList.flattenCurrencies().map { it.currency.id } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index a8c3a91d43..83b44bf068 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -8,17 +8,18 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* -internal class MultiWalletWarningsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class MultiWalletWarningsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, @@ -30,14 +31,20 @@ internal class MultiWalletWarningsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) // Wait until the wallet appears in the list - stateHolder.uiState.first { + stateController.uiState.first { it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateController.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = warnings, + notifications = persistentListOf(), + ), + ) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( userWalletId = userWallet.walletId, @@ -46,4 +53,9 @@ internal class MultiWalletWarningsSubscriber( ) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletWarningsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 6f564c38a2..9e389948bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -1,62 +1,48 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.common.extensions.isZero import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import timber.log.Timber +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import java.math.BigDecimal -@Deprecated("Use PrimaryCurrencySubscriberV2 instead") -internal class PrimaryCurrencySubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class PrimaryCurrencySubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val stateController: WalletStateController, private val analyticsEventHandler: AnalyticsEventHandler, -) : WalletSubscriber() { +) : BasicSingleWalletSubscriber() { - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { + override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, + flow = getPrimaryCurrencyStatusFlow(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::Pair, ) - .onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } - - updateContent(status, maybeCurrencyStatusAndAppCurrency.second) + .onEach { (status, appCurrency) -> + updateContent(status, appCurrency) sendAnalyticsEvent(status) - checkWalletWithFunds(status) } } private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateHolder.update( + stateController.update( SetPrimaryCurrencyTransformer( status = status, userWallet = userWallet, @@ -81,11 +67,11 @@ internal class PrimaryCurrencySubscriber( -> null } - cardBalanceState?.let { + cardBalanceState?.let { balanceState -> // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, + balance = balanceState, tokensCount = null, ), ) @@ -100,7 +86,8 @@ internal class PrimaryCurrencySubscriber( } } - private suspend fun checkWalletWithFunds(status: CryptoCurrencyStatus) { - if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): PrimaryCurrencySubscriber } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt deleted file mode 100644 index 3aa5c39bb4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.common.extensions.isZero -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach -import java.math.BigDecimal - -internal class PrimaryCurrencySubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val stateController: WalletStateController, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = getPrimaryCurrencyStatusFlow(), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::Pair, - ) - .onEach { (status, appCurrency) -> - updateContent(status, appCurrency) - sendAnalyticsEvent(status) - } - } - - private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateController.update( - SetPrimaryCurrencyTransformer( - status = status, - userWallet = userWallet, - appCurrency = appCurrency, - ), - ) - } - - private fun sendAnalyticsEvent(status: CryptoCurrencyStatus) { - val fiatAmount = status.value.fiatAmount - val cardBalanceState = when (status.value) { - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - -> createCardBalanceState(fiatAmount) - is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate - is CryptoCurrencyStatus.Unreachable, - -> AnalyticsParam.CardBalanceState.BlockchainError - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Loading, - is CryptoCurrencyStatus.Custom, - -> null - } - - cardBalanceState?.let { - // do not send tokens count for single currency wallet - analyticsEventHandler.send( - event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, - tokensCount = null, - ), - ) - } - } - - private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? { - return when { - fiatAmount == null -> null - fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty - else -> AnalyticsParam.CardBalanceState.Full - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index af71e2d23f..c00cc2569d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -1,56 +1,54 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onEach -@Deprecated("Use SingleWalletButtonsSubscriberV2 instead") -internal class SingleWalletButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class SingleWalletButtonsSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) : WalletSubscriber() { + private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) override fun create(coroutineScope: CoroutineScope): Flow { - return channelFlow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> - getCryptoCurrencyActionsUseCase(userWallet = userWallet, status = status) - ?.let { send(it) } + return getPrimaryCurrencyStatusFlow() + .flatMapLatest { + getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) } - } - .onEach { actions -> - updateContent( - tokenActionsState = actions, - portfolioId = PortfolioId(userWallet.walletId), - ) + .onEach { + updateContent(tokenActionsState = it) } } - private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateHolder.update( + private fun updateContent(tokenActionsState: TokenActionsState) { + stateController.update( SetCryptoCurrencyActionsTransformer( tokenActionsState = tokenActionsState, userWallet = userWallet, clickIntents = clickIntents, - portfolioId = portfolioId, + accountId = accountId, ), ) } - private suspend fun getCryptoCurrencyActionsUseCase(userWallet: UserWallet, status: CryptoCurrencyStatus) = - this.getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) - .conflate() - .distinctUntilChanged() - .firstOrNull() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletButtonsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt deleted file mode 100644 index 85d65823fb..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.TokenActionsState -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.SetCryptoCurrencyActionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.onEach - -internal class SingleWalletButtonsSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow { - return getPrimaryCurrencyStatusFlow() - .flatMapLatest { - getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) - } - .onEach { - updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) - } - } - - private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateController.update( - SetCryptoCurrencyActionsTransformer( - tokenActionsState = tokenActionsState, - userWallet = userWallet, - clickIntents = clickIntents, - portfolioId = portfolioId, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index d417654a2f..c8b4e2fd21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -1,93 +1,93 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError 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.SetExpressStatusesTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import timber.log.Timber +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList") -@Deprecated("Use SingleWalletExpressStatusesSubscriberV2 instead") -internal class SingleWalletExpressStatusesSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletExpressStatusesSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, -) : WalletSubscriber() { - - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { - return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, - ).onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> { + val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> getOnrampTransactionsUseCase( userWalletId = userWallet.walletId, - cryptoCurrencyId = status.currency.id, - ).onEach { maybeTransaction -> + cryptoCurrencyId = currencyStatus.currency.id, + ) + .map { currencyStatus to it } + } + + return combine( + flow = getOnrampTransactionsFlow, + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::toTriple, + ) + .onEach { (status, maybeTransaction, appCurrency) -> maybeTransaction.fold( ifRight = { onrampTxs -> onrampTxs.clearHiddenTerminal() - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, onrampTxs = onrampTxs, clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ifLeft = { - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, - onrampTxs = listOf(), + onrampTxs = emptyList(), clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ) } - .launchIn(coroutineScope) - } + } + + private fun toTriple(firstPair: Pair, second: C): Triple { + return Triple(firstPair.first, firstPair.second, second) } private suspend fun List.clearHiddenTerminal() { - this.filter { it.status.isHidden && it.status.isTerminal } + this + .filter { it.status.isHidden && it.status.isTerminal } .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletExpressStatusesSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt deleted file mode 100644 index 59581e98ce..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.onramp.model.cache.OnrampTransaction -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.SetExpressStatusesTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@Suppress("LongParameterList") -internal class SingleWalletExpressStatusesSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> { - val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> - getOnrampTransactionsUseCase( - userWalletId = userWallet.walletId, - cryptoCurrencyId = currencyStatus.currency.id, - ) - .map { currencyStatus to it } - } - - return combine( - flow = getOnrampTransactionsFlow, - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::toTriple, - ) - .onEach { (status, maybeTransaction, appCurrency) -> - maybeTransaction.fold( - ifRight = { onrampTxs -> - onrampTxs.clearHiddenTerminal() - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = onrampTxs, - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ifLeft = { - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = listOf(), - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ) - } - } - - private fun toTriple(firstPair: Pair, second: C): Triple { - return Triple(firstPair.first, firstPair.second, second) - } - - private suspend fun List.clearHiddenTerminal() { - this - .filter { it.status.isHidden && it.status.isTerminal } - .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index d815a9c21c..ac34381df5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -7,7 +7,11 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarni import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate @@ -17,9 +21,10 @@ import kotlinx.coroutines.flow.onEach /** [REDACTED_AUTHOR] */ -internal class SingleWalletNotificationsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class SingleWalletNotificationsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val clickIntents: WalletClickIntents, @@ -30,10 +35,15 @@ internal class SingleWalletNotificationsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateController.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletNotificationsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt new file mode 100644 index 0000000000..88e0697c7e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +internal class SingleWalletSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet.Cold, + override val accountDependencies: AccountDependencies, + override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + override val stateController: WalletStateController, + override val clickIntents: WalletClickIntents, +) : BasicAccountListSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow = combine( + flow = getAccountStatusListFlow(), + flow2 = getAppCurrencyFlow(), + flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), + flow4 = accountDependencies.isAccountsModeEnabledUseCase(), + transform = ::updateState2, + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletSubscriber + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt deleted file mode 100644 index af21e47dc4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -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 -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -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 kotlinx.coroutines.CoroutineScope - -@Deprecated("Use SingleWalletWithTokenSubscriber instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenListSubscriber( - private val userWallet: UserWallet.Cold, - private val tokenListStore: MultiWalletTokenListStore, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - return tokenListStore.getOrThrow(userWallet.walletId) - } - - override suspend fun onTokenListReceived(maybeTokenList: Lce) = Unit -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt similarity index 88% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt index 44957d2db0..1a31f1f1b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt @@ -12,7 +12,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -internal class SingleWalletWithTokenSubscriber @AssistedInject constructor( +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class SingleWalletWithTokenSubscriberLegacy @AssistedInject constructor( @Assisted override val userWallet: UserWallet.Cold, override val accountDependencies: AccountDependencies, override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -30,6 +31,6 @@ internal class SingleWalletWithTokenSubscriber @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenSubscriber + fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenSubscriberLegacy } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 1dcc11ff2a..04f33d8338 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -6,6 +6,7 @@ import com.tangem.domain.pay.model.MainCustomerInfoContentState import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber @Suppress("LongParameterList") @@ -28,10 +30,14 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( private val clickIntents: WalletClickIntents, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val analytics: WalletTangemPayAnalyticsEventSender, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> { + coroutineScope.launch { + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) + } return subscribeOnTangemPayInfoUpdates() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt deleted file mode 100644 index bef7b6defb..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import arrow.core.Either -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.txhistory.models.TxHistoryListError -import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map - -typealias MaybeTxHistoryCount = Either -typealias MaybeTxHistoryItems = Either>> - -@Suppress("LongParameterList") -@Deprecated("Use TxHistorySubscriberV2 instead") -internal class TxHistorySubscriber( - private val userWallet: UserWallet.Cold, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, -) : WalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow> { - return flow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> - val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - ) - - setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - - maybeTxHistoryItemCount.onRight { - val maybeTxHistoryItems = txHistoryItemsUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - refresh = isRefresh, - ).map { it.cachedIn(coroutineScope) } - - setLoadedTxHistoryState(maybeTxHistoryItems, status.currency) - } - } - } - } - - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { - stateHolder.update( - maybeTxHistoryItemCount.fold( - ifLeft = { error -> - SetTxHistoryCountErrorTransformer( - userWallet = userWallet, - error = error, - pendingTransactions = status.value.pendingTransactions, - currency = status.currency, - clickIntents = clickIntents, - ) - }, - ifRight = { txCount -> - SetTxHistoryCountTransformer( - userWalletId = userWallet.walletId, - transactionsCount = txCount, - clickIntents = clickIntents, - ) - }, - ), - ) - } - - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { - stateHolder.update( - maybeTxHistoryItems.fold( - ifLeft = { - SetTxHistoryItemsErrorTransformer( - userWalletId = userWallet.walletId, - error = it, - clickIntents = clickIntents, - ) - }, - ifRight = { itemsFlow -> - val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - val itemConverter = TxHistoryItemStateConverter( - symbol = blockchain.currency, - decimals = blockchain.decimals(), - clickIntents = clickIntents, - currency = currency, - ) - - SetTxHistoryItemsTransformer( - userWallet = userWallet, - flow = itemsFlow.map { items -> - items.map(itemConverter::convert) - }, - clickIntents = clickIntents, - ) - }, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt index 78c388f87f..ba0efd30e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt @@ -3,12 +3,15 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map +import arrow.core.Either import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -18,19 +21,23 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHis import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList") -internal class TxHistorySubscriberV2( - override val userWallet: UserWallet.Cold, +internal class TxHistorySubscriberLegacy @AssistedInject constructor( + @Assisted override val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val isRefresh: Boolean, private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, ) : BasicSingleWalletSubscriber() { @@ -45,7 +52,7 @@ internal class TxHistorySubscriberV2( setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - maybeTxHistoryItemCount.onRight { + maybeTxHistoryItemCount.onRight { _ -> val maybeTxHistoryItems = txHistoryItemsUseCase( userWalletId = userWallet.walletId, currency = status.currency, @@ -58,7 +65,10 @@ internal class TxHistorySubscriberV2( } } - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { + private fun setLoadingTxHistoryState( + maybeTxHistoryItemCount: Either, + status: CryptoCurrencyStatus, + ) { stateController.update( maybeTxHistoryItemCount.fold( ifLeft = { error -> @@ -81,13 +91,16 @@ internal class TxHistorySubscriberV2( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { + private fun setLoadedTxHistoryState( + maybeTxHistoryItems: Either>>, + currency: CryptoCurrency, + ) { stateController.update( maybeTxHistoryItems.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, - error = it, + error = error, clickIntents = clickIntents, ) }, @@ -111,4 +124,9 @@ internal class TxHistorySubscriberV2( ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): TxHistorySubscriberLegacy + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 34b9a3bc97..500b3bcd98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -1,32 +1,35 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository 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.RemoveNFTCollectionsTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -@Deprecated("Use WalletNFTListSubscriberV2 instead") -internal class WalletNFTListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class WalletNFTListSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, -) : WalletSubscriber() { +) : BasicWalletSubscriber() { @OptIn(ExperimentalCoroutinesApi::class) override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( - walletsRepository.nftEnabledStatus(userWallet.walletId), - currenciesRepository.getWalletCurrenciesUpdates(userWallet.walletId), - ) { nftEnabled, currencies -> nftEnabled to currencies } + flow = walletsRepository.nftEnabledStatus(userWallet.walletId), + flow2 = getCryptoCurrencyStatusesFlow(), + transform = ::Pair, + ) .distinctUntilChanged() .flatMapLatest { (nftEnabled, currencies) -> // if NFT is enabled for this wallet and there are currencies, @@ -38,21 +41,26 @@ internal class WalletNFTListSubscriber( started = SharingStarted.WhileSubscribed(), replay = 1, ) - .onEach { - stateHolder.update( + .onEach { walletNFTCollections -> + stateController.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, - nftCollections = it, + nftCollections = walletNFTCollections.flattenCollections, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } } else { // otherwise, hide NFT from wallet - stateHolder.update( + stateController.update( RemoveNFTCollectionsTransformer(userWallet.walletId), ) emptyFlow() } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): WalletNFTListSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt deleted file mode 100644 index 88ccbc686f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.wallets.repository.WalletsRepository -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.RemoveNFTCollectionsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -internal class WalletNFTListSubscriberV2 @AssistedInject constructor( - @Assisted override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val walletsRepository: WalletsRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, -) : BasicWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( - flow = walletsRepository.nftEnabledStatus(userWallet.walletId), - flow2 = getCryptoCurrencyStatusesFlow(), - transform = ::Pair, - ) - .distinctUntilChanged() - .flatMapLatest { (nftEnabled, currencies) -> - // if NFT is enabled for this wallet and there are currencies, - // then start observing changes from store and apply transformer if need - if (nftEnabled && currencies.isNotEmpty()) { - getNFTCollectionsUseCase.invokeForAccounts(userWallet.walletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - .onEach { walletNFTCollections -> - stateController.update( - SetNFTCollectionsTransformer( - userWalletId = userWallet.walletId, - nftCollections = walletNFTCollections.flattenCollections, - onItemClick = { clickIntents.onNFTClick(userWallet) }, - ), - ) - } - } else { - // otherwise, hide NFT from wallet - stateController.update( - RemoveNFTCollectionsTransformer(userWallet.walletId), - ) - emptyFlow() - } - } - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet): WalletNFTListSubscriberV2 - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt new file mode 100644 index 0000000000..57f51e06d1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +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.GetWalletNotificationsCarouselFactory +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class WalletNotificationsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val clickIntents: WalletClickIntents, + private val getWalletNotificationsFactory: GetWalletNotificationsFactory, + private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory, + private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow> { + return combine( + flow = getWalletNotificationsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), + flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate() + .distinctUntilChanged(), + ) { notifications, notificationsCarousel -> + val displayedWalletUM = stateHolder.getWalletUM(userWallet.walletId) + + // Wait until the wallet appears in the list + stateHolder.uiState.first { + it.wallets2.any { walletUM -> walletUM.walletsBalanceUM.id == userWallet.walletId } + } + + // If there are notifications, we need to filter out the RateApp notification from stackable notifications, + // because it should not be shown together with other notifications. + val alteredNotificationsCarousel = if (notifications.isNotEmpty()) { + notificationsCarousel.filterNot { it is WalletNotificationUM.RateApp } + } else { + notificationsCarousel + }.toPersistentList() + + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = persistentListOf(), + notifications = notifications, + notificationsCarousel = alteredNotificationsCarousel, + ), + ) + + val totalNotifications = (notifications + alteredNotificationsCarousel).toPersistentList() + + walletWarningsAnalyticsSender.send(displayedWalletUM, totalNotifications) + walletWarningsSingleEventSender.send( + userWalletId = userWallet.walletId, + displayedWalletUM = displayedWalletUM, + newNotifications = totalNotifications, + ) + + totalNotifications + } + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): WalletNotificationsSubscriber + } +} \ 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 9f4df0e402..838acf9e43 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 @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* @@ -17,7 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolli import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @Composable -internal fun WalletEventEffect( +internal fun WalletEventEffectLegacy( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, event: StateEvent, @@ -59,6 +60,53 @@ internal fun WalletEventEffect( is WalletEvent.RequestPushPermissions -> { showPermissionRequest = value.onAllow to value.onDeny } + is WalletEvent.CollapseBalance -> { /* no-op */ } + } + }, + ) +} + +@Composable +internal fun WalletEventEffect( + walletsPagerState: PagerState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + onCollapseBalance: () -> Unit, +) { + val resources = LocalContext.current.resources + + var showPermissionRequest by remember { mutableStateOf Unit, () -> Unit>?>(null) } + HandlePermissionRequest( + permissionRequestParams = showPermissionRequest, + onPermissionRequestResult = { showPermissionRequest = null }, + ) + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + walletsPagerState.animateScrollToPage(page = value.newIndex) + } + is WalletEvent.ChangeWalletWithoutScroll -> { + walletsPagerState.scrollToPage(page = value.newIndex) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is WalletEvent.CopyAddress -> { + snackbarHostState.showSnackbar( + message = resources.getStringSafe(R.string.wallet_notification_address_copied), + duration = SnackbarDuration.Short, + ) + } + is WalletEvent.DemonstrateWalletsScrollPreview -> { + /* no-op */ + } + is WalletEvent.RequestPushPermissions -> { + showPermissionRequest = value.onAllow to value.onDeny + } + is WalletEvent.CollapseBalance -> onCollapseBalance() } }, ) 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 ec373aea09..5eb943f097 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 @@ -21,10 +21,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -64,16 +69,13 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.core.ui.test.MarketTooltipTestTags import com.tangem.core.ui.utils.TangemSharedTransitionLayout -import com.tangem.core.ui.utils.lineTo -import com.tangem.core.ui.utils.moveTo import com.tangem.core.ui.utils.toPx import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.accountScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.accountScreenWithEmptyTokensState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder -import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections @@ -111,7 +113,7 @@ internal fun WalletScreen( onBottomSheetStateChange = onBottomSheetStateChange, ) - WalletEventEffect( + WalletEventEffectLegacy( walletsListState = walletsListState, snackbarHostState = snackbarHostState, event = state.event, @@ -304,11 +306,7 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val background = if (state.isNewMarketEnabled) { - TangemTheme.colors.background.tertiary - } else { - TangemTheme.colors.background.primary - } + val background = TangemTheme.colors.background.tertiary val showMarketsHint by remember { derivedStateOf { @@ -401,17 +399,10 @@ private inline fun BaseScaffoldWithMarkets( } BottomSheetScrim( - color = if (state.showMarketsOnboarding) { - Color.Black.copy(alpha = .65f) - } else { - Color.Black.copy(alpha = .40f) - }, - visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || - state.showMarketsOnboarding, + color = Color.Black.copy(alpha = .40f), + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded, onDismissRequest = { - if (!state.showMarketsOnboarding) { - coroutineScope.launch { bottomSheetState.partialExpand() } - } + coroutineScope.launch { bottomSheetState.partialExpand() } }, ) @@ -419,7 +410,7 @@ private inline fun BaseScaffoldWithMarkets( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 8.dp) - .padding(horizontal = 16.dp) + .padding(horizontal = 12.dp) .fillMaxWidth(), isVisible = state.showMarketsOnboarding, availableHeight = maxHeight, @@ -503,7 +494,7 @@ private fun MarketsTooltip( } @Composable -internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { +private fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier, visible = isVisible, @@ -532,33 +523,22 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { @Composable private fun MarketsTooltipContent(onCloseClick: () -> Unit, modifier: Modifier = Modifier) { - val backgroundColor = TangemTheme.colors.background.primary - val cornerRadius = CornerRadius(x = 16.dp.toPx()) + val backgroundColor = TangemTheme.colors.background.action val tipDpSize = DpSize(width = 20.dp, height = 8.dp) + val tooltipShape = remember(tipDpSize) { TooltipShape(cornerRadius = 16.dp, tipSize = tipDpSize) } Row( modifier = modifier - .padding(bottom = tipDpSize.height) - .drawBehind { - val rect = size.toRect() - val tipSize = tipDpSize.toSize() - val tipRect = Rect( - offset = Offset( - x = rect.center.x - tipSize.center.x, - y = rect.bottom, - ), - size = tipSize, - ) - drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius) - - val tipPath = Path().apply { - moveTo(tipRect.topLeft) - lineTo(tipRect.bottomCenter) - lineTo(tipRect.topRight) - } - drawPath(color = backgroundColor, path = tipPath) - } - .padding(all = 12.dp), + .shadow( + elevation = TangemTheme.dimens.elevation12, + shape = tooltipShape, + clip = false, + ambientColor = Color.Black.copy(alpha = 0.7f), + ) + .background(backgroundColor, tooltipShape) + .clickable(interactionSource = null, indication = null, onClick = {}) + .padding(all = 12.dp) + .padding(bottom = tipDpSize.height), horizontalArrangement = Arrangement.spacedBy(space = 12.dp), verticalAlignment = Alignment.Top, ) { @@ -599,6 +579,32 @@ private fun MarketsTooltipContent(onCloseClick: () -> Unit, modifier: Modifier = } } +private class TooltipShape( + private val cornerRadius: Dp, + private val tipSize: DpSize, +) : Shape { + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { + val cornerRadiusPx = with(density) { cornerRadius.toPx() } + val tipWidth = with(density) { tipSize.width.toPx() } + val tipHeight = with(density) { tipSize.height.toPx() } + val bodyHeight = size.height - tipHeight + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(left = 0f, top = 0f, right = size.width, bottom = bodyHeight), + cornerRadius = CornerRadius(cornerRadiusPx), + ), + ) + moveTo(size.width / 2 - tipWidth / 2, bodyHeight) + lineTo(size.width / 2, size.height) + lineTo(size.width / 2 + tipWidth / 2, bodyHeight) + close() + } + return Outline.Generic(path) + } +} + @Composable private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { val alpha by animateFloatAsState( @@ -720,8 +726,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { when (bottomSheetConfig.content) { - is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) - is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt new file mode 100644 index 0000000000..a874aec01e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -0,0 +1,548 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.semantics.clearAndSetSemantics +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 androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.components.rememberIsKeyboardVisible +import com.tangem.core.ui.components.sheetscaffold.* +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.* +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsHint +import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsTooltip +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletBalance +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletListContent +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletPagerIndicator +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletTopBar +import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver +import kotlinx.coroutines.launch +import kotlin.math.abs + +private const val MARKET_HINT_THRESHOLD = 0.5f + +@OptIn(ExperimentalDecomposeApi::class) +@Composable +internal fun WalletScreen2( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } + + val snackbarHostState = remember(::SnackbarHostState) + val walletsPagerState = rememberPagerState( + initialPage = state.selectedWalletIndex, + pageCount = { state.wallets2.size }, + ) + + val partialCollapsedHeight = 64.dp + statusBarHeight + val balanceBlockHeight = 320.dp + partialCollapsedHeight + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = balanceBlockHeight, + partialCollapsedHeight = partialCollapsedHeight, + snapAnimationSpec = spring(stiffness = Spring.StiffnessMedium), + ) + + val coroutineScope = rememberCoroutineScope() + + WalletContent2( + state = state, + walletsPagerState = walletsPagerState, + snackbarHostState = snackbarHostState, + behavior = behavior, + bottomSheetContent = bottomSheetContent, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + ) + + WalletEventEffect( + walletsPagerState = walletsPagerState, + snackbarHostState = snackbarHostState, + event = state.event, + onCollapseBalance = { + if (behavior.state.collapsedFraction < 1f) { + coroutineScope.launch { + behavior.state.collapse() + } + } + }, + ) +} + +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") +@Composable +private fun WalletContent2( + state: WalletScreenState, + walletsPagerState: PagerState, + behavior: TangemCollapsingAppBarBehavior, + snackbarHostState: SnackbarHostState, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), +) { + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + + var walletBalance by remember { mutableStateOf(TextReference.EMPTY) } + + BaseScaffoldWithMarkets( + state = state, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, + appBarContent = { + WalletTopBar( + topBarConfig = state.topBarConfig, + walletBalance = walletBalance, + behavior = behavior, + ) + }, + ) { paddingValues, bottomSheetState -> + val marketHintApproxHeight = 140.dp + + val contentPadding = PaddingValues( + bottom = paddingValues.calculateBottomPadding() + marketHintApproxHeight, + ) + + LaunchedEffect(walletsPagerState.currentPage) { + if (walletsPagerState.currentPage != state.selectedWalletIndex) { + state.onWalletChange(walletsPagerState.currentPage, false) + } + } + + val listStates = rememberSaveable(saver = lazyListStateMapSaver(walletsPagerState.pageCount)) { + mutableMapOf().apply { + repeat(walletsPagerState.pageCount) { index -> put(index, LazyListState()) } + } + } + + val canPagerScroll by remember { derivedStateOf { behavior.state.heightOffset == 0f } } + + Box( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = -1f), + ) { + NorthernLightsBackground( + containerColor = if (LocalIsInDarkTheme.current) { + TangemTheme.colors2.surface.level1 + } else { + TangemTheme.colors2.surface.level2 + }, + modifier = Modifier.matchParentSize(), + ) + + WalletPagerIndicator( + pagerState = walletsPagerState, + behavior = behavior, + ) + + HorizontalPager( + state = walletsPagerState, + userScrollEnabled = canPagerScroll, + beyondViewportPageCount = 1, + ) { currentWalletIndex -> + val listState = listStates[currentWalletIndex] ?: rememberLazyListState() + + val currentWallet = state.wallets2.getOrElse(currentWalletIndex) { + state.wallets2[state.selectedWalletIndex] + } + + LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) { + if (walletsPagerState.currentPage == currentWalletIndex) { + walletBalance = (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar + } + } + + val isShowMarketsHint by remember { + derivedStateOf { + behavior.state.collapsedFraction > MARKET_HINT_THRESHOLD && + listState.layoutInfo.totalItemsCount > 0 && + !listState.canScrollBackward && !listState.canScrollForward || + listState.canScrollBackward && !listState.canScrollForward + } + } + + val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex) + + Box( + modifier = Modifier.alpha(pageSlideAlpha), + ) { + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + WalletBalance( + behavior = behavior, + walletBalanceUM = currentWallet.walletsBalanceUM, + buttons = currentWallet.buttons, + isBalanceHidden = state.isHidingMode, + ) + }, + body = { + WalletListContent( + currentWallet = currentWallet, + listState = listState, + isBalanceHidden = state.isHidingMode, + contentPadding = contentPadding, + modifier = Modifier + .fillMaxSize() + .nestedScroll(behavior.nestedScrollConnection), + ) + }, + ) + + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + MarketsHint( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = peekHeight + TangemTheme.dimens2.x7), + isVisible = isShowMarketsHint, + ) + } + } + + MarketsTooltip( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp) + .padding(horizontal = 12.dp) + .fillMaxWidth(), + isVisible = state.showMarketsOnboarding, + availableHeight = LocalWindowSize.current.height, + bottomSheetState = bottomSheetState, + onCloseClick = state.onDismissMarketsTooltip, + ) + } + } +} + +@Suppress("LongParameterList", "LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private inline fun BaseScaffoldWithMarkets( + state: WalletScreenState, + snackbarHostState: SnackbarHostState, + bottomSheetHeaderHeightProvider: () -> Dp, + modifier: Modifier = Modifier, + noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, + crossinline appBarContent: @Composable () -> Unit, + crossinline bottomSheetContent: @Composable () -> Unit, + crossinline content: @Composable (PaddingValues, TangemSheetState) -> Unit, +) { + val bottomSheetState = rememberTangemStandardBottomSheetState() + + val isKeyboardVisible by rememberIsKeyboardVisible() + + val scaffoldState = rememberTangemBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() } + val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() } + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val maxHeight = LocalWindowSize.current.height + + val coroutineScope = rememberCoroutineScope() + val background = TangemTheme.colors2.surface.level3 + + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) }, + ) { + val backgroundColor = LocalMainBottomSheetColor.current + var isSearchFieldFocused by remember { mutableStateOf(false) } + val isNavBarVisible = remember { mutableStateOf(true) } + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + onBottomSheetStateChange = onBottomSheetStateChange, + navigationBarVisible = isNavBarVisible, + isSearchFieldFocused = isSearchFieldFocused, + ) + + Box(modifier = modifier) { + TangemBottomSheetScaffold( + snackbarHost = { snackbarHostState -> + WalletSnackbarHost( + snackbarHostState = snackbarHostState, + event = state.event, + modifier = Modifier + .padding(bottom = TangemTheme.dimens2.x1) + .navigationBarsPadding(), + ) + }, + containerColor = Color.Unspecified, + sheetContainerColor = backgroundColor.value, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetShape = TangemTheme.shapes.bottomSheetLarge, + sheetContent = { + // hide bottom sheet when back pressed + BackHandler( + isKeyboardVisible.not() && + bottomSheetState.currentValue == TangemSheetValue.Expanded, + ) { + coroutineScope.launch { bottomSheetState.partialExpand() } + } + + Column( + modifier = Modifier + // expand bottom sheet when clicked on the header + .clickable( + enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, + indication = null, + interactionSource = null, + ) { + coroutineScope.launch { bottomSheetState.expand() } + } + .sizeIn(maxHeight = maxHeight - statusBarHeight), + ) { + Hand(Modifier.drawBehind { drawRect(backgroundColor.value) }) + + Box( + modifier = Modifier + .onFocusChanged { + isSearchFieldFocused = it.isFocused + }, + ) { + bottomSheetContent() + } + } + }, + content = { paddingValues -> + content(paddingValues, bottomSheetState) + appBarContent() + + BottomSheetScrim( + color = Color.Black.copy(alpha = .40f), + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded, + onDismissRequest = { + coroutineScope.launch { bottomSheetState.partialExpand() } + }, + ) + }, + ) + + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = isNavBarVisible.value, + ) { + Box( + Modifier + .align(Alignment.BottomCenter) + .background(backgroundColor.value) + .height(bottomBarHeight) + .fillMaxWidth(), + ) + } + } + + LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { + if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { + state.onDismissMarketsTooltip() + } + } + } +} + +@Composable +private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = tween(), + label = "scrim", + ) + val dismissSheet = if (visible) { + Modifier + .pointerInput(onDismissRequest) { + detectTapGestures { + onDismissRequest() + } + } + .clearAndSetSemantics {} + } else { + Modifier + } + Canvas( + Modifier + .fillMaxSize() + .then(dismissSheet), + ) { + drawRect(color = color, alpha = alpha) + } +} + +@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod") +@Composable +private fun BottomSheetStateEffects( + bottomSheetState: TangemSheetState, + navigationBarVisible: MutableState, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + isSearchFieldFocused: Boolean, +) { + LaunchedEffect(bottomSheetState.targetValue) { + when (bottomSheetState.targetValue) { + TangemSheetValue.Hidden, + TangemSheetValue.Expanded, + -> navigationBarVisible.value = false + TangemSheetValue.PartiallyExpanded, + -> navigationBarVisible.value = true + } + } + + // expand bottom sheet when keyboard appears + val isKeyboardVisible by rememberIsKeyboardVisible() + + LaunchedEffect(isKeyboardVisible) { + if (isKeyboardVisible && isSearchFieldFocused) { + bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(Unit) { + snapshotFlow { + bottomSheetState.currentValue == TangemSheetValue.Expanded && + bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + }.collect { sheetHasBeenHidden -> + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + } + + val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + LaunchedEffect(isSheetHidden) { + onBottomSheetStateChange( + if (isSheetHidden) { + BottomSheetState.COLLAPSED + } else { + BottomSheetState.EXPANDED + }, + ) + } +} + +@Composable +private fun WalletSnackbarHost( + snackbarHostState: SnackbarHostState, + event: StateEvent, + modifier: Modifier = Modifier, +) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data -> + if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) { + CopiedTextSnackbar(data) + } else { + TangemSnackbar(data) + } + } +} + +@Composable +private fun rememberPageAlpha(pagerState: PagerState, currentPageIndex: Int): State { + return remember { + derivedStateOf { + val pageOffset = pagerState.currentPageOffsetFraction + val currentPage = pagerState.currentPage + + when { + // Current page is being swiped away + currentPageIndex == currentPage -> { + 1f - abs(pageOffset) * 2f + } + // Target page is being swiped in + currentPageIndex == pagerState.targetPage -> { + (abs(pageOffset) * 2f - 1f).coerceAtLeast(0f) + } + // Other pages remain invisible + else -> 0f + }.coerceIn(0f, 1f) + } + } +} + +// region Preview +@OptIn(ExperimentalDecomposeApi::class) +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider::class) data: WalletScreenState) { + TangemThemePreviewRedesign { + WalletScreen2( + state = data, + bottomSheetContent = { + Text("Markets Content") + }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, + ) + } +} + +private class WalletScreen2PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + WalletScreenPreviewData.defaultState, + WalletScreenPreviewData.defaultState.copy(selectedWalletIndex = 1), + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt new file mode 100644 index 0000000000..32c059a463 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.impl.R + +private const val STARS_INLINE_CONTENT_ID = "stars" + +@Composable +internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 300)), + exit = fadeOut(animationSpec = tween(durationMillis = 300)), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Swipe up to explore the market", // todo redesign main lokalise + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.primary, + textAlign = TextAlign.Center, + ) + Text( + text = buildAnnotatedString { + append("Find new hidden gems ") // todo redesign main lokalise + appendInlineContent( + STARS_INLINE_CONTENT_ID, + alternateText = "\uDBC0\uDDBF", + ) + }, + inlineContent = mapOf( + STARS_INLINE_CONTENT_ID to InlineTextContent( + placeholder = Placeholder( + width = TangemTheme.typography2.bodyRegular14.fontSize, + height = TangemTheme.typography2.bodyRegular14.fontSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + children = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_magic_default_24), + tint = TangemTheme.colors2.text.neutral.tertiary, + contentDescription = null, + ) + }, + ), + ), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + textAlign = TextAlign.Center, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsHint_Preview() { + TangemThemePreviewRedesign { + MarketsHint( + isVisible = true, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt new file mode 100644 index 0000000000..b5d5ac4170 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt @@ -0,0 +1,193 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +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.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.sheetscaffold.TangemSheetState +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.MarketTooltipTestTags +import com.tangem.core.ui.utils.toPx +import com.tangem.feature.wallet.impl.R +import kotlinx.coroutines.delay +import kotlin.math.roundToInt + +@Composable +internal fun MarketsTooltip( + availableHeight: Dp, + bottomSheetState: TangemSheetState, + isVisible: Boolean, + onCloseClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val tooltipOffset by remember { + derivedStateOf { + val bottomSheetOffset = try { + // Can throw exception during the first composition + with(density) { bottomSheetState.requireOffset().toDp() } + } catch (e: Exception) { + 0.dp + } + + bottomSheetOffset - availableHeight + } + } + + var isVisibleWrapped by remember { mutableStateOf(value = false) } + LaunchedEffect(isVisible) { + if (isVisible) { + delay(timeMillis = 300) + } + + isVisibleWrapped = isVisible + } + + val slideOffset = 40.dp.toPx() + AnimatedVisibility( + modifier = modifier + .offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) } + .testTag(MarketTooltipTestTags.CONTAINER), + visible = isVisibleWrapped, + enter = slideIn( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + visibilityThreshold = IntOffset.VisibilityThreshold, + ), + initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) }, + ) + fadeIn(), + exit = fadeOut(), + ) { + MarketsTooltipContent(onCloseClick = onCloseClick) + } +} + +@Composable +internal fun MarketsTooltipContent(onCloseClick: () -> Unit, modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.action + val tipDpSize = DpSize(width = 20.dp, height = 8.dp) + val tooltipShape = remember(tipDpSize) { TooltipShape(cornerRadius = 16.dp, tipSize = tipDpSize) } + + Row( + modifier = modifier + .shadow( + elevation = TangemTheme.dimens.elevation12, + shape = tooltipShape, + clip = false, + ambientColor = Color.Black.copy(alpha = 0.7f), + ) + .background(backgroundColor, tooltipShape) + .clickable(interactionSource = null, indication = null, onClick = {}) + .padding(all = 12.dp) + .padding(bottom = tipDpSize.height), + horizontalArrangement = Arrangement.spacedBy(space = 12.dp), + verticalAlignment = Alignment.Top, + ) { + Icon( + modifier = Modifier.size(size = 18.dp), + painter = painterResource(id = R.drawable.ic_plus_18), + tint = Color.Unspecified, + contentDescription = null, + ) + 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, + ) + } +} + +private class TooltipShape( + private val cornerRadius: Dp, + private val tipSize: DpSize, +) : Shape { + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { + val cornerRadiusPx = with(density) { cornerRadius.toPx() } + val tipWidth = with(density) { tipSize.width.toPx() } + val tipHeight = with(density) { tipSize.height.toPx() } + val bodyHeight = size.height - tipHeight + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(left = 0f, top = 0f, right = size.width, bottom = bodyHeight), + cornerRadius = CornerRadius(cornerRadiusPx), + ), + ) + moveTo(size.width / 2 - tipWidth / 2, bodyHeight) + lineTo(size.width / 2, size.height) + lineTo(size.width / 2 + tipWidth / 2, bodyHeight) + close() + } + return Outline.Generic(path) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsTooltip_Preview() { + TangemThemePreviewRedesign { + MarketsTooltipContent(onCloseClick = {}) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt deleted file mode 100644 index ea93bb097c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.getDefaultRowColors -import com.tangem.core.ui.components.getWarningRowColors -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig -import kotlinx.collections.immutable.ImmutableList - -@Composable -internal fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config = config) { - ActionsBottomSheetContent(actions = it.actions) - } -} - -@Composable -private fun ActionsBottomSheetContent(actions: ImmutableList) { - Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - actions.forEach { action -> - if (action.enabled) { - val rowColors = if (action.isWarning) { - getWarningRowColors() - } else { - getDefaultRowColors() - } - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconResId, - enabled = action.enabled, - rowColors = rowColors, - onItemsClick = action.onClick, - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ActionsBottomSheetContent_Light( - @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) - config: ActionsBottomSheetConfig, -) { - TangemThemePreview { - // Use preview of content because ModalBottomSheet isn't supported in Preview mode - ActionsBottomSheetContent(actions = config.actions) - } -} - -private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.actionsBottomSheet), -) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt new file mode 100644 index 0000000000..443bd5f959 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.ui.ds.button.TangemButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock + +internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) { + (state as? WalletUM.Content)?.let { content -> + item(key = "NFTCollections", contentType = "NFTCollections") { + WalletNFTItem( + modifier = itemModifier, + state = content.nftState, + ) + } + } +} + +internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifier) { + val organizeButton = state.tokensListUM.organizeButtonUM + if (organizeButton != null) { + item( + key = "OrganizeTokensButton", + contentType = "OrganizeTokensButton", + ) { + TangemButton( + organizeButton, + modifier = itemModifier, + ) + } + } +} + +internal fun LazyListScope.tangemPay(walletUM: WalletUM, isBalanceHiding: Boolean, modifier: Modifier = Modifier) { + if (walletUM is WalletState.MultiCurrency) { + item( + key = "TangemPayMainScreenBlock", + contentType = walletUM.tangemPayState::class.java, + ) { + TangemPayMainScreenBlock( + state = walletUM.tangemPayState, + isBalanceHidden = isBalanceHiding, + modifier = modifier, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt new file mode 100644 index 0000000000..8759ecdf11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt @@ -0,0 +1,422 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.layoutId +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM.Content.CollectionPreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletNFTItem2(state: WalletNFTItemUM, modifier: Modifier = Modifier) { + val nftModifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(TangemTheme.colors2.surface.level3) + when (state) { + is WalletNFTItemUM.Hidden -> Unit + is WalletNFTItemUM.Empty -> WalletNFTItemEmpty( + modifier = nftModifier, + onClick = state.onItemClick, + ) + is WalletNFTItemUM.Failed -> WalletNFTItemFailed(modifier = nftModifier) + is WalletNFTItemUM.Loading -> WalletNFTItemLoading(modifier = nftModifier) + + is WalletNFTItemUM.Content -> WalletNFTItemContent( + state = state, + onClick = state.onItemClick, + modifier = nftModifier, + ) + } +} + +@Composable +private fun WalletNFTItemEmpty(onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Image( + painter = painterResource(R.drawable.img_nft_empty_collection), + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .layoutId(TangemRowLayoutId.HEAD), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_receive_nft), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemContent(state: WalletNFTItemUM.Content, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Box(modifier = Modifier.layoutId(TangemRowLayoutId.HEAD)) { + CollectionsPreviews( + previews = state.previews, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe( + id = R.string.nft_wallet_count, + state.allAssetsCount, + state.collectionsCount, + ), + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.secondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_unable_to_load), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + } +} + +@Composable +private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary) + .layoutId(TangemRowLayoutId.HEAD), + ) + TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + radius = TangemTheme.dimens2.x25, + + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size110), + ) + TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size80), + ) + } +} + +@Composable +@Suppress("MagicNumber", "ReusedModifierInstance") +private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { + val modifiers = when (previews.size) { + 1 -> previews1Modifiers() + 2 -> previews2Modifiers() + 3 -> previews3Modifiers() + else -> previews4Modifiers() + } + Box( + modifier = modifier + .size(TangemTheme.dimens2.x10), + ) { + previews.take(modifiers.size).forEachIndexed { index, s -> + val previewModifier = modifiers[index] + when (s) { + is CollectionPreview.Image -> { + SubcomposeAsyncImage( + modifier = previewModifier, + model = s.url, + loading = { + RectangleShimmer() + }, + error = { + Box( + modifier = previewModifier.background(TangemTheme.colors2.surface.level2), + ) + }, + contentDescription = null, + ) + } + is CollectionPreview.More -> { + Icon( + modifier = previewModifier + .background(TangemTheme.colors2.surface.level2), + imageVector = ImageVector.vectorResource(R.drawable.ic_nft_preview_more_16), + tint = TangemTheme.colors2.text.neutral.secondary, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun previews1Modifiers(): List = listOf( + Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)), +) + +@Composable +private fun BoxScope.previews2Modifiers(): List = listOf( + Modifier + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(TangemTheme.dimens2.x0_5) + .clip(RoundedCornerShape(topStart = 10.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.BottomEnd), +) + +@Composable +private fun BoxScope.previews3Modifiers(): List = listOf( + Modifier + .padding(start = 3.dp, top = 3.dp) + .size(17.8.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(top = 10.dp, end = 1.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(TangemTheme.dimens2.x0_5) + .size(18.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopEnd), + Modifier + .padding(start = 9.dp, top = 2.dp) + .size(14.dp) + .clip(RoundedCornerShape(4.dp)) + .align(Alignment.BottomStart), +) + +@Composable +private fun BoxScope.previews4Modifiers(): List = listOf( + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopEnd), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomEnd), +) + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider2::class) state: WalletNFTItemUM) { + TangemThemePreviewRedesign { + WalletNFTItem2( + state = state, + modifier = Modifier + .background(TangemTheme.colors2.surface.level1), + ) + } +} + +private class WalletNFTItemProvider2 : CollectionPreviewParameterProvider( + collection = listOf( + WalletNFTItemUM.Empty( + onItemClick = { }, + ), + WalletNFTItemUM.Loading, + WalletNFTItemUM.Failed, + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = true, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.Image("img4"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.More, + ), + allAssetsCount = 125, + collectionsCount = 11, + isFlickering = true, + noCollectionAssetsCount = 0, + onItemClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index bddfb23fb1..2febad643b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard import kotlinx.collections.immutable.ImmutableList @@ -106,7 +106,7 @@ private fun Preview_WalletsList() { TangemThemePreview { WalletsList( lazyListState = rememberLazyListState(), - wallets = WalletPreviewData.wallets.values.toPersistentList(), + wallets = WalletPreviewDataLegacy.wallets.values.toPersistentList(), isBalanceHidden = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt new file mode 100644 index 0000000000..0d4a5903ce --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -0,0 +1,210 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +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 +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed +import com.tangem.core.ui.extensions.orEmpty +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview +import com.tangem.feature.wallet.presentation.preview.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import com.tangem.utils.StringsSigns +import kotlinx.collections.immutable.ImmutableList + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletBalance( + walletBalanceUM: WalletBalanceUM, + behavior: TangemCollapsingAppBarBehavior, + buttons: ImmutableList, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + val collapsedFraction = behavior.state.collapsedFraction + val alpha = 1f - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .alpha(alpha) + .scale(scale) + .snapToExitUntilCollapsed(behavior) + .fillMaxWidth() + .padding(top = 64.dp) + .statusBarsPadding(), + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + ) { + Balance( + walletBalanceUM = walletBalanceUM, + isBalanceHidden = isBalanceHidden, + ) + SpacerH(TangemTheme.dimens2.x3) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + Text( + text = walletBalanceUM.name, + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + TangemDeviceIcon(state = walletBalanceUM.deviceIcon) + } + } + SpacerH(TangemTheme.dimens2.x2) + ActionButtons(buttons) + SpacerH(TangemTheme.dimens2.x6) + } +} + +@Composable +private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = walletBalanceUM, + label = "Update the balance", + modifier = modifier.testTag(MainScreenTestTags.WALLET_BALANCE), + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { balanceUM -> + when (balanceUM) { + is WalletBalanceUM.Content -> { + Text( + text = balanceUM.balance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + style = TangemTheme.typography2.titleRegular44.applyBladeBrush( + isEnabled = balanceUM.isBalanceFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography2.bodySemibold15.fontSize, + maxFontSize = TangemTheme.typography2.titleRegular44.fontSize, + ), + ) + } + is WalletBalanceUM.Error -> { + Text( + text = StringsSigns.DASH_SIGN, + style = TangemTheme.typography2.titleRegular44, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography2.bodySemibold15.fontSize, + maxFontSize = TangemTheme.typography2.titleRegular44.fontSize, + ), + ) + } + is WalletBalanceUM.Loading, + -> { + TextShimmer( + text = "123456", + style = TangemTheme.typography2.titleRegular44, + radius = TangemTheme.dimens2.x25, + textSizeHeight = true, + ) + } + } + } +} + +@Composable +private fun ActionButtons(buttons: ImmutableList) { + Row( + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + buttons.fastForEach { button -> + key(button.text) { + Column( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2_5), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SecondaryTangemButton( + iconRes = button.iconRes, + onClick = button.onClick, + shape = TangemButtonShape.Rounded, + ) + Text( + text = button.text.orEmpty().resolveReference(), + style = TangemTheme.typography2.bodySemibold15, + color = TangemTheme.colors2.text.neutral.primary, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WalletBalance_Preview(@PreviewParameter(WalletBalancePreviewProvider::class) params: WalletBalanceUM) { + TangemThemePreviewRedesign { + WalletBalance( + walletBalanceUM = params, + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + buttons = WalletPreviewData.actionButtons, + isBalanceHidden = false, + ) + } +} + +private class WalletBalancePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + WalletBalancePreview.content, + WalletBalancePreview.content.copy(isBalanceFlickering = true), + WalletBalancePreview.loading, + WalletBalancePreview.error, + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt deleted file mode 100644 index ff2d45177d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.common - -import android.content.res.Configuration -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 androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig - -/** - * Wallet bottom sheet with detail notification information - * - * @param config component config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: WalletBottomSheetConfig -> - BottomSheetContent(config = content) - } -} - -@Composable -private fun BottomSheetContent(config: WalletBottomSheetConfig) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size48), - tint = when (config) { - is WalletBottomSheetConfig.UnlockWallets -> TangemTheme.colors.icon.primary1 - }, - ) - - Column( - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = config.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - Text( - text = config.subtitle.resolveReference(), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body2, - ) - } - - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { - val buttonModifier = Modifier.fillMaxWidth() - - PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) - - SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) - } - } -} - -@Composable -private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - PrimaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - PrimaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -@Composable -private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - SecondaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - SecondaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun WalletBottomSheetContent_Preview( - @PreviewParameter(WalletBottomSheetConfigProvider::class) - config: WalletBottomSheetConfig, -) { - TangemThemePreview { - // Use preview of content because ModalBottomSheet isn't supported in Preview mode - BottomSheetContent(config = config) - } -} - -private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.bottomSheet.content as WalletBottomSheetConfig), -) -// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 01044070d2..651707a1c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.* @@ -45,7 +44,7 @@ import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDropDownItems @@ -375,22 +374,22 @@ private fun Preview_WalletCard( private class WalletCardStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.walletCardContentState, - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState, + WalletPreviewDataLegacy.walletCardContentState.copy( balance = "0.00", ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( title = "Title", additionalInfo = WalletAdditionalInfo( hideable = false, content = TextReference.Str("3 cards"), ), ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( isBalanceFlickering = true, ), - WalletPreviewData.walletCardLoadingState, - WalletPreviewData.walletCardErrorState, + WalletPreviewDataLegacy.walletCardLoadingState, + WalletPreviewDataLegacy.walletCardErrorState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 5620f17592..3d542d67a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -1,12 +1,77 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems +import com.tangem.common.ui.notifications.notifications +import com.tangem.common.ui.notifications.notificationsCarousel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems +import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun WalletListContent( + currentWallet: WalletUM, + isBalanceHidden: Boolean, + listState: LazyListState, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + val containerColor = TangemTheme.colors2.surface.level1 + + val movableItemModifier = Modifier.padding(horizontal = TangemTheme.dimens2.x3) + val itemModifier = movableItemModifier.padding(top = TangemTheme.dimens2.x3) + + LazyColumn( + modifier = modifier, + state = listState, + contentPadding = contentPadding, + horizontalAlignment = Alignment.CenterHorizontally, + overscrollEffect = rememberOverscrollEffect(), + ) { + notifications( + notifications = currentWallet.notifications.map { it.messageUM }.toPersistentList(), + contentColor = containerColor, + modifier = movableItemModifier, + ) + notificationsCarousel( + containerColor = containerColor, + modifier = movableItemModifier, + notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(), + ) + + tangemPay( + walletUM = currentWallet, + isBalanceHiding = isBalanceHidden, + modifier = itemModifier, + ) + + tokensListItems2( + walletTokensListUM = currentWallet.tokensListUM, + modifier = movableItemModifier, + isBalanceHidden = isBalanceHidden, + ) + + nftCollections2(state = currentWallet, itemModifier = itemModifier) + + organizeTokens2(state = currentWallet, itemModifier = itemModifier) + } +} /** * Wallet content diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt new file mode 100644 index 0000000000..d047533bb3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.TangemPagerIndicator +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletPagerIndicator(pagerState: PagerState, behavior: TangemCollapsingAppBarBehavior) { + val collapsedFraction = behavior.state.collapsedFraction + val alpha = MAX_SCALE - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + + Box( + modifier = Modifier + .graphicsLayer { + scaleY = scale + translationY = behavior.state.heightOffset + } + .fillMaxWidth() + .height( + with(LocalDensity.current) { + behavior.state.heightOffsetLimit.toDp().unaryMinus() + }, + ) + .alpha(alpha), + ) { + TangemPagerIndicator( + pagerState = pagerState, + modifier = Modifier + .padding(top = 248.dp) + .scale(scaleY = 1f, scaleX = scale) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index cb76a6809e..7782f92342 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -1,24 +1,82 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import android.content.res.Configuration +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import dev.chrisbanes.haze.HazeProgressive + +private const val VISIBILITY_THRESHOLD = 0.5f + +/** + * Wallet screen collapsing top bar + * + * @param topBarConfig top bar config + * @param walletBalance wallet balance text reference + * @param behavior collapsing behavior + */ +@Composable +internal fun WalletTopBar( + topBarConfig: WalletTopBarConfig, + walletBalance: TextReference?, + behavior: TangemCollapsingAppBarBehavior, +) { + Surface( + color = Color.Unspecified, + contentColor = Color.Unspecified, + modifier = Modifier.hazeEffectTangem { + progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + }, + ) { + val wrappedBalance = remember(behavior.state.collapsedFraction) { + if (behavior.state.collapsedFraction > VISIBILITY_THRESHOLD) walletBalance else null + } + + TangemTopBar( + title = wrappedBalance, + startActionUM = TangemTopBarActionUM( + iconRes = R.drawable.ic_tangem_24, + isActionable = false, + ), + endActionUM = TangemTopBarActionUM( + iconRes = R.drawable.ic_more_default_24, + isActionable = true, + onClick = topBarConfig.onDetailsClick, + ghostModeProgress = behavior.state.collapsedFraction, + ), + modifier = Modifier + .statusBarsPadding() + .testTag(MainScreenTestTags.TOP_BAR), + ) + } +} /** * Wallet screen top bar * * @param config component config */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WalletTopBar(config: WalletTopBarConfig) { @@ -46,6 +104,21 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { @Composable private fun Preview_WalletTopBar() { TangemThemePreview { - WalletTopBar(config = WalletPreviewData.topBarConfig) + WalletTopBar(config = WalletPreviewDataLegacy.topBarConfig) } -} \ No newline at end of file +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WalletTopBar_Preview() { + TangemThemePreviewRedesign { + WalletTopBar( + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + walletBalance = stringReference("$ 8923,05"), + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 1ddf7dd2cf..f8349a879c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -3,22 +3,23 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc import androidx.compose.animation.* import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -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.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme @@ -58,7 +59,8 @@ internal fun LazyListScope.portfolioTokensList( portfolioIndex = portfolioIndex, isBalanceHidden = isBalanceHidden, ) - if (tokens.isEmpty()) { + val portfolioContent = portfolio.content + if (portfolioContent is PortfolioItemContentUM.Empty) { item( key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", @@ -76,9 +78,7 @@ internal fun LazyListScope.portfolioTokensList( ), visible = isExpanded, ) { - NonContentItemContent( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28), - ) + EmptyAccountContent(portfolioContent) } } return @@ -114,6 +114,28 @@ internal fun LazyListScope.portfolioTokensList( ) } +@Composable +private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH16() + NonContentItemContent() + val emptyAction = emptyConent.action + if (emptyAction != null) { + SpacerH16() + SecondarySmallButton( + config = SmallButtonConfig( + text = emptyAction.text, + onClick = { emptyAction.onClick() }, + ), + ) + } + SpacerH24() + } +} + @Suppress("MagicNumber") private fun LazyListScope.portfolioItem( portfolio: TokensListItemUM.Portfolio, @@ -163,7 +185,7 @@ private fun LazyListScope.portfolioItem( @Suppress("MagicNumber") @Composable -private fun SlideInItemVisibility( +internal fun SlideInItemVisibility( visible: Boolean, currentIndex: Int, lastIndex: Int, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 7d1be99d66..5041832a6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,5 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency +import androidx.compose.animation.* +import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -8,22 +12,38 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.currency.icon.CurrencyIconState 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.stringResourceSafe +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import kotlinx.collections.immutable.ImmutableList internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -59,6 +79,181 @@ internal fun LazyListScope.tokensListItems( } } +/** + * LazyList extension for [WalletTokensListState] + * + * @param walletTokensListUM state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.tokensListItems2( + walletTokensListUM: WalletTokensListUM, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + when (walletTokensListUM) { + is WalletTokensListUM.Loading, + is WalletTokensListUM.Content, + -> { + walletTokensListUM.tokenList.fastForEachIndexed { index, listItem -> + when (listItem) { + is TokensListItemUM2.GroupTitle, + is TokensListItemUM2.Token, + -> tokenItem( + listItem = listItem, + index = index, + lastIndex = walletTokensListUM.tokenList.lastIndex, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + is TokensListItemUM2.Portfolio -> portfolioItem( + listItem = listItem, + index = index, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + } + } + WalletTokensListUM.Empty -> nonContentItem(modifier = modifier) + } +} + +private fun LazyListScope.tokenItem( + listItem: TokensListItemUM2, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val itemModifier = modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .padding(top = if (index == 0) TangemTheme.dimens2.x3 else 0.dp) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = index, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + + when (val tokenRowUM = listItem.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + modifier = itemModifier, + ) + } + } +} + +private fun LazyListScope.portfolioItem( + listItem: TokensListItemUM2.Portfolio, + index: Int, + isBalanceHidden: Boolean, + modifier: Modifier, +) { + val lastIndex = listItem.tokenList.lastIndex + 1 + + accountItem( + listItem = listItem, + modifier = modifier, + index = index, + lastIndex = lastIndex, + isBalanceHidden = isBalanceHidden, + ) + itemsIndexed( + items = listItem.tokenList, + key = { _, item -> item.tokenRowUM.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, item -> + SlideInItemVisibility( + currentIndex = tokenIndex + 1, + lastIndex = lastIndex, + modifier = modifier + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = tokenIndex + 1, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ), + visible = listItem.isExpanded, + ) { + val itemModifier = Modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = tokenIndex + 1 } + + when (val tokenRowUM = item.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + modifier = itemModifier, + ) + } + } + }, + ) +} + +private fun LazyListScope.accountItem( + listItem: TokensListItemUM2.Portfolio, + modifier: Modifier, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val portfolioModifier = modifier + .padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3) + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .roundedShapeItemDecoration( + currentIndex = 0, + radius = 18.dp, + addDefaultPadding = false, + lastIndex = if (listItem.isExpanded) lastIndex else 0, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + if (listItem.isCollapsable) { + PortfolioRowItem( + item = listItem, + isBalanceHidden = isBalanceHidden, + modifier = portfolioModifier, + ) + } else { + TangemHeaderRow( + title = (listItem.tokenRowUM.titleUM as? TangemTokenRowUM.TitleUM.Content)?.text.orEmpty(), + subtitle = (listItem.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + headTangemIconUM = listItem.tokenRowUM.headIconUM, + modifier = portfolioModifier, + ) + } + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -77,6 +272,7 @@ private fun LazyListScope.contentItems( currentIndex = index, lastIndex = items.lastIndex, backgroundColor = TangemTheme.colors.background.primary, + radius = 18.dp, ) .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) .semantics { lazyListItemPosition = index }, @@ -85,6 +281,117 @@ private fun LazyListScope.contentItems( ) } +@Suppress("MagicNumber", "ReusedModifierInstance", "LongMethod") +@Composable +internal fun PortfolioRowItem( + item: TokensListItemUM2.Portfolio, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + // TangemSharedTransitionLayout { + ProvideSharedTransitionScope(modifier) { + val iconSharedContentState = rememberSharedContentState(key = "icon") + val titleSharedContentState = rememberSharedContentState(key = "title") + val boundsTransform = BoundsTransform { _, _ -> tween(250) } + + AnimatedContent( + item.isExpanded, + transitionSpec = { + fadeIn(animationSpec = tween(350, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(350))) + }, + ) { isExpandedWrapped -> + val animatedContentScope = this + + val composables = remember { + SharedTokenRowComposables( + icon = { modifier -> + val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default + val currencyIconState = + when (val currencyIconState = item.tokenRowUM.headIconUM.currencyIconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> + currencyIconState.copy(size = size) + is CurrencyIconState.CryptoPortfolio.Letter -> + currencyIconState.copy(size = size) + else -> currencyIconState + } + + TangemIcon( + tangemIconUM = item.tokenRowUM.headIconUM.copy(currencyIconState = currencyIconState), + modifier = modifier.sharedBounds( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), + ) + }, + title = { modifier -> + val targetAnimationFraction = if (isExpandedWrapped) 0f else 1f + + val animationFraction = animateFloatAsState( + targetValue = targetAnimationFraction, + animationSpec = tween(durationMillis = 350), + ) + + val startStyle = TangemTheme.typography2.captionSemibold12 + val stopStyle = TangemTheme.typography2.bodySemibold16 + + val textStyle by remember(animationFraction.value) { + derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) } + } + + val resizedTitle = when (val titleUM = item.tokenRowUM.titleUM) { + is TangemTokenRowUM.TitleUM.Content -> titleUM.copy( + text = styledStringReference( + titleUM.text.resolveReference(), + { textStyle.toSpanStyle() }, + ), + ) + else -> titleUM + } + + TokenRowTitle( + titleUM = resizedTitle, + modifier = modifier.sharedBounds( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), + ), + ) + }, + ) + } + + if (isExpandedWrapped) { + TangemHeaderRow( + subtitle = (item.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + titleContent = composables.title, + headContent = composables.icon, + tailUM = TangemRowTailUM.Icon(R.drawable.ic_minimize_24), + onItemClick = item.tokenRowUM.onItemClick, + ) + } else { + TangemTokenRow( + tokenRowUM = item.tokenRowUM, + headComponent = composables.icon, + titleComponent = composables.title, + isBalanceHidden = isBalanceHidden, + reorderableState = null, + ) + } + } + // } + } +} + +@Stable +class SharedTokenRowComposables( + val title: @Composable (Modifier) -> Unit, + val icon: @Composable (Modifier) -> Unit, +) + private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( key = NON_CONTENT_TOKENS_LIST_KEY, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index b8f7e9a587..fbf1117370 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @Composable @@ -36,6 +38,17 @@ private fun TangemPayMainScreenBlockPreview() { TangemThemePreview { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.RefreshNeeded( + TangemPayRefreshNeeded( + tangemIcon = R.drawable.ic_tangem_24, + buttonText = resourceReference(id = R.string.home_button_scan), + onRefreshClick = {}, + shouldShowProgress = false, + ), + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt index d1b702c3b9..d4ff2fd0cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -4,6 +4,9 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import com.tangem.utils.extensions.mapNotNullValues /** * Animate scroll [LazyListState]. @@ -28,4 +31,32 @@ private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newI private fun LazyListLayoutInfo.getItemSizeWithSpacing(): Int { return viewportSize.width - afterContentPadding - beforeContentPadding + mainAxisItemSpacing +} + +/** + * Saver for [LazyListState] map, where key is page index, and value is [LazyListState] of this page. + */ +internal fun lazyListStateMapSaver(pageCount: Int): Saver, Any> { + return mapSaver( + save = { map -> + map.mapKeys { it.key.toString() } + .mapValues { listState -> + listState.value.firstVisibleItemIndex to listState.value.firstVisibleItemScrollOffset + } + }, + restore = { restoredMap -> + @Suppress("UNCHECKED_CAST") + val typedMap = restoredMap as? Map> ?: return@mapSaver null + + typedMap.mapKeys { it.key.toInt() } + .mapNotNullValues { (_, value) -> + val (index, offset) = value + LazyListState(index, offset) + } + .toMutableMap() + .apply { + repeat(pageCount) { putIfAbsent(it, LazyListState()) } + } + }, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index b163d627d6..0242c1bf72 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -6,7 +6,6 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetWalletTotalBalanceUseCaseV2 import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError @@ -20,7 +19,6 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R @@ -39,8 +37,6 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList") internal class DefaultUserWalletsFetcher @AssistedInject constructor( getWalletsUseCase: GetWalletsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getWalletTotalBalanceUseCaseV2: GetWalletTotalBalanceUseCaseV2, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -103,11 +99,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( // We should not load balances in auth mode flowOf(Lce.Loading(walletIds.associateWith { TotalFiatBalance.Loading })) } else { - if (accountsFeatureToggles.isFeatureEnabled) { - getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) - } else { - getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged() - } + getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) } } diff --git a/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml new file mode 100644 index 0000000000..62d66ecd28 --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt index efb582f714..cbdb878996 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.google.common.truth.Truth.assertThat import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -22,7 +23,7 @@ class YieldSupplyPromoBannerConverterTest { val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) val tokenList = ungroupedTokenList(status) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = tokenList, ) val converter = YieldSupplyPromoBannerConverter( @@ -40,7 +41,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -58,7 +59,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(statusActive), ) val converter = YieldSupplyPromoBannerConverter( @@ -86,7 +87,7 @@ class YieldSupplyPromoBannerConverterTest { ) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(statusSmall, statusBig), ) val converter = YieldSupplyPromoBannerConverter( @@ -109,7 +110,7 @@ class YieldSupplyPromoBannerConverterTest { val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -127,7 +128,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM") val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index fd948afd0b..48f84373fe 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { /** Domain models */ implementation(projects.domain.account) + implementation(projects.domain.account.status) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.blockaid.models) diff --git a/features/walletconnect/impl/detekt-baseline-debug.xml b/features/walletconnect/impl/detekt-baseline-debug.xml index 017f85e30e..2d5009d5c4 100644 --- a/features/walletconnect/impl/detekt-baseline-debug.xml +++ b/features/walletconnect/impl/detekt-baseline-debug.xml @@ -27,12 +27,10 @@ MultilineLambdaItParameter:WcSwitchNetworkModel.kt$WcSwitchNetworkModel${ if (it.isExistInWcSession) { router.pop() } else { showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) } } NamedArguments:WcAddEthereumChainModalBottomSheetContent.kt$WcAddEthereumChainModalBottomSheetContent(state, {}, {}, {}) NamedArguments:WcAlertsFactory.kt$WcAlertsFactory$createMaliciousDAppAlert(alertType.description, alertType.onClick, alertType.iconType, alertType.iconBgType) - NamedArguments:WcPairModel.kt$WcPairModel$handlePairState( pairState, portfolios, selected, isAccountMode, ) NamedArguments:WcSendTransactionModel.kt$WcSendTransactionModel$buildUiState(securityCheck, useCase, signState, isApprovalMethod) NestedScopeFunctions:WcSendAndReceiveBlockAidUiConverter.kt$WcSendAndReceiveBlockAidUiConverter$let { spendAllowanceUMConverter.convert( WcSpendAllowanceUMConverter.Input( approvedAmount = it, onLearnMoreClick = value.onApproveLearnMoreClick, ), ) } NoNameShadowing:WcNavigationUtils.kt$model NullCheckOnMutableProperty:WcCommonTransactionComponentDelegate.kt$WcCommonTransactionComponentDelegate$if (contentStack != null) { val content by contentStack!!.subscribeAsState() BackHandler(onBack = ::onChildBack) content.active.instance.BottomSheet() } - NullableBooleanCheck:WcPairModel.kt$WcPairModel$isAccountMode ?: false NullableToStringCall:WcEstimatedWalletChangeUMConverter.kt$WcEstimatedWalletChangeUMConverter$${value.sign} ReusedModifierInstance:DefaultWalletConnectEntryComponent.kt$DefaultWalletConnectEntryComponent$Content(modifier = modifier) ReusedModifierInstance:WcAppInfoBS.kt$Box( modifier = modifier .padding(start = 48.dp) .border( width = 2.dp, color = TangemTheme.colors.background.action, shape = CircleShape, ) .padding(2.dp) .background(color = TangemTheme.colors.background.action) .size(20.dp) .clip(CircleShape) .background(color = TangemTheme.colors.icon.primary1.copy(alpha = 0.1F)), ) { Text( modifier = Modifier.align(Alignment.Center), text = "+$remainingCount", style = TangemTheme.typography.overline, color = TangemTheme.colors.text.secondary, ) } @@ -46,7 +44,6 @@ UnsafeCallOnNullableType:WcPairComponent.kt$WcPairComponent$model.portfolioFetcher!! UnsafeCallOnNullableType:WcSignTransactionComponent.kt$WcSignTransactionComponent$content!! UnsafeCallOnNullableType:WcTransactionRequestInfoComponent.kt$WcTransactionRequestInfoComponent$content!! - UseEmptyCounterpart:WcPairModel.kt$WcPairModel$setOf<Network>() UseOrEmpty:WcSpendAllowanceUMConverter.kt$WcSpendAllowanceUMConverter$value.approvedAmount.amount?.currencySymbol ?: "" UseOrEmpty:WcTransactionCheckErrorItem.kt$notification.text?.resolveReference() ?: "" diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index 39193ee405..1675ff7fba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -15,11 +15,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent.* -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.* import com.tangem.features.walletconnect.connections.model.WcPairModel import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes.Alert @@ -68,7 +66,6 @@ internal class WcPairComponent( else -> model.stackNavigation.pop() } is WcAppInfoRoutes.SelectNetworks, - is WcAppInfoRoutes.SelectWallet, is WcAppInfoRoutes.PortfolioSelector, -> model.stackNavigation.pop() } @@ -104,18 +101,10 @@ internal class WcPairComponent( callback = model, ), ) - is WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent( - appComponentContext = appComponentContext, - params = WcSelectWalletComponent.WcSelectWalletParams( - selectedWalletId = config.selectedWalletId, - onDismiss = ::dismiss, - callback = model, - ), - ) WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create( context = appComponentContext, params = PortfolioSelectorComponent.Params( - portfolioFetcher = model.portfolioFetcher!!, + portfolioFetcher = model.portfolioFetcher, bsCallback = model.portfolioSelectorCallback, controller = model.selectorController, ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt deleted file mode 100644 index 93e8566ec0..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt +++ /dev/null @@ -1,216 +0,0 @@ -package com.tangem.features.walletconnect.connections.components - -import android.content.res.Configuration -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.walletconnect.connections.model.WcSelectWalletModel -import com.tangem.features.walletconnect.impl.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -internal class WcSelectWalletComponent( - appComponentContext: AppComponentContext, - private val params: WcSelectWalletParams, -) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { - - private val model: WcSelectWalletModel = getOrCreateModel(params = params) - - override fun dismiss() { - params.onDismiss() - } - - @Composable - override fun BottomSheet() { - val state by model.state.collectAsStateWithLifecycle() - WcSelectWalletModalBS( - wallets = state.wallets, - selectedWalletId = state.selectedUserWalletId, - onBack = router::pop, - onDismiss = ::dismiss, - ) - } - - interface ModelCallback { - fun onWalletSelected(userWalletId: UserWalletId) - } - - data class WcSelectWalletParams( - val selectedWalletId: UserWalletId, - val callback: ModelCallback, - val onDismiss: () -> Unit, - ) -} - -@Composable -private fun WcSelectWalletModalBS( - wallets: ImmutableList, - selectedWalletId: UserWalletId, - onBack: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - if (wallets.isEmpty()) return - - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onDismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - onBack = onBack, - containerColor = TangemTheme.colors.background.primary, - title = { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.common_choose_wallet), - startIconRes = R.drawable.ic_back_24, - onStartClick = onBack, - ) - }, - content = { - WcSelectWalletContent( - modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - wallets = wallets, - selectedWalletId = selectedWalletId, - ) - }, - ) -} - -@Composable -private fun WcSelectWalletContent( - wallets: ImmutableList, - selectedWalletId: UserWalletId, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - wallets.fastForEach { state -> - key(state.id) { - val baseModifier = Modifier - .clip(RoundedCornerShape(14.dp)) - .clickable(onClick = state.onClick) - val itemModifier = if (state.id == selectedWalletId.stringValue) { - baseModifier.border( - width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(14.dp), - ) - } else { - baseModifier - } - UserWalletItem( - modifier = itemModifier, - state = state, - blockColors = TangemBlockCardColors.copy( - containerColor = Color.Unspecified, - disabledContainerColor = Color.Unspecified, - ), - ) - } - } - } -} - -@Suppress("LongMethod") -@Composable -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun WcSelectWalletContent_Preview() { - val wallets = persistentListOf( - UserWalletItemUM( - id = "user_wallet_1", - name = stringReference("Tangem 2.0"), - information = getInformation(42), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_2", - name = stringReference("Tangem White"), - information = getInformation(24), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_3", - name = stringReference("Bitcoin"), - information = getInformation(1), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = getInformation(21), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = UserWalletItemUM.Information.Loading, - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = UserWalletItemUM.Information.Failed, - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - ) - TangemThemePreview { - WcSelectWalletModalBS( - wallets = wallets, - selectedWalletId = UserWalletId(wallets.first().id.encodeToByteArray()), - onBack = {}, - onDismiss = {}, - ) - } -} - -private fun getInformation(tokenCount: Int): UserWalletItemUM.Information.Loaded { - val text = TextReference.PluralRes( - id = R.plurals.card_label_token_count, - count = tokenCount, - formatArgs = wrappedList(tokenCount), - ) - return UserWalletItemUM.Information.Loaded(text) -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index cf9d41811a..16a0bfeda6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -27,9 +27,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val verifiedDAppState: VerifiedDAppState, val appSubtitle: String, val notification: WcAppInfoSecurityNotification?, - val portfolioSelectRow: PortfolioSelectUM?, - val walletName: String, - val onWalletClick: (() -> Unit)?, + val portfolioSelectRow: PortfolioSelectUM, val networksInfo: WcNetworksInfo, val onNetworksClick: () -> Unit, override val connectButtonConfig: WcPrimaryButtonConfig, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 57298f9b5d..d82ef65634 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -18,7 +18,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.qrscanning.models.QrResultSource @@ -55,7 +54,6 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val multiAccountListSupplier: MultiAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - accountsFeatureToggles: AccountsFeatureToggles, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, analytics: AnalyticsEventHandler, @@ -70,11 +68,7 @@ internal class WcConnectionsModel @Inject constructor( init { analytics.send(WcAnalyticEvents.ScreenOpened()) listenQrUpdates() - if (accountsFeatureToggles.isFeatureEnabled) { - listenWcSessions() - } else { - listenWcSessionsOld() - } + listenWcSessions() } private fun listenQrUpdates() { @@ -98,21 +92,6 @@ internal class WcConnectionsModel @Inject constructor( .launchIn(modelScope) } - private fun listenWcSessionsOld() { - wcSessionsUseCase.invoke() - .conflate() - .distinctUntilChanged() - .onEach { sessionsMap -> - uiState.update( - WcSessionsTransformer( - sessionsMap = sessionsMap, - openAppInfoModal = ::openAppInfoModal, - ), - ) - } - .launchIn(modelScope) - } - private fun listenWcSessions() { combine( flow = wcSessionsUseCase.invoke().distinctUntilChanged(), 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 d686883940..a8bc0da03c 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 @@ -21,17 +21,11 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.producer.SingleAccountListProducer -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.network.Network 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.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.Unknown @@ -41,16 +35,17 @@ import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.account.PortfolioSelectorController import com.tangem.features.walletconnect.connections.components.WcPairComponent import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig -import com.tangem.features.walletconnect.connections.model.transformers.* +import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter +import com.tangem.features.walletconnect.connections.model.transformers.WcNetworksSelectedTransformer import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -62,11 +57,8 @@ import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate internal interface WcPairComponentCallback : - WcSelectWalletComponent.ModelCallback, WcSelectNetworksComponent.ModelCallback -private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2 - @Stable @ModelScoped @Suppress("LongParameterList", "LargeClass") @@ -75,13 +67,10 @@ internal class WcPairModel @Inject constructor( private val messageSender: UiMessageSender, override val dispatchers: CoroutineDispatcherProvider, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, val selectorController: PortfolioSelectorController, - private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, portfolioFetcherFactory: PortfolioFetcher.Factory, wcPairUseCaseFactory: WcPairUseCase.Factory, - getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, ) : Model(), WcPairComponentCallback { @@ -95,47 +84,39 @@ internal class WcPairModel @Inject constructor( ) val stackNavigation = StackNavigation() - val portfolioFetcher: PortfolioFetcher? + val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), + scope = modelScope, + ) val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { override val onDismiss: () -> Unit = { stackNavigation.pop() } override val onBack: () -> Unit = { stackNavigation.pop() } } - private val selectedUserWalletFlow: MutableStateFlow by lazy { - MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) - } private val selectedPortfolio = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() - private var additionallyEnabledNetworks = setOf() + private var additionallyEnabledNetworks = emptySet() private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert) val appInfoUiState: StateFlow field = MutableStateFlow(createLoadingState()) init { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), - scope = modelScope, - ) - modelScope.launch { - val params = SingleAccountListProducer.Params(params.userWalletId) - val accountList = singleAccountListSupplier.getSyncOrNull(params) - if (accountList == null) { - router.pop() - return@launch + modelScope.launch { + val portfolioBalance = portfolioFetcher.data.first().balances + .firstNotNullOfOrNull { (walletId, balance) -> + if (params.userWalletId == walletId) balance else null } - val firstAccount = accountList.accounts.first() - selectorController.selectAccount(firstAccount.accountId) - combineFlows(portfolioFetcher) + if (portfolioBalance == null) { + router.pop() + return@launch } - } else { - portfolioFetcher = null - loadDAppInfo() + selectorController.selectAccount(portfolioBalance.accountsBalance.mainAccount.accountId) + combineFlows(portfolioFetcher) } } @@ -143,35 +124,31 @@ internal class WcPairModel @Inject constructor( combine( flow = portfolioFetcher.data, flow2 = selectorController.selectedAccountWithData(portfolioFetcher) - .distinctUntilChanged() .filterNotNull() .onEach { selectedPortfolio.tryEmit(it) } - .onEach { stackNavigation.pop() }, + .runningReduce { _, new -> + stackNavigation.pop() + new + }, flow3 = wcPairUseCase(), flow4 = isAccountsModeEnabledUseCase(), transform = { portfolios, selected, pairState, isAccountMode -> handlePairState( - pairState, - portfolios, - selected, - isAccountMode, + pairState = pairState, + portfolios = portfolios, + selected = selected, + isAccountMode = isAccountMode, ) }, ) .launchIn(modelScope) } - private fun loadDAppInfo() { - wcPairUseCase() - .onEach { pairState -> handlePairState(pairState) } - .launchIn(modelScope) - } - private suspend fun handlePairState( pairState: WcPairState, - portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, - isAccountMode: Boolean? = null, + portfolios: PortfolioFetcher.Data, + selected: Pair, + isAccountMode: Boolean, ) { when (pairState) { is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate( @@ -193,41 +170,37 @@ internal class WcPairModel @Inject constructor( ) processError(pairState.error) } - is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode ?: false) } + is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode == true) } is WcPairState.Proposal -> handleProposalState( pairState = pairState, portfolios = portfolios, selected = selected, + isAccountMode = isAccountMode, ) } } private suspend fun handleProposalState( pairState: WcPairState.Proposal, - portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + portfolios: PortfolioFetcher.Data, + selected: Pair, + isAccountMode: Boolean, ) { - val availableWallets = pairState.dAppSession.proposalNetwork.keys - .filter { !it.isLocked && it.isMultiCurrency } sessionProposal = pairState.dAppSession - val selectedUserWalletFlow = this.selectedUserWalletFlow - val portfolioWallet = selected?.first - val portfolioAccount = selected?.second - val portfolioAccountId = portfolioAccount?.account?.accountId + val portfolioAccount = selected.second + val portfolioAccountId = portfolioAccount.account.accountId val proposalAccountNetwork = sessionProposal.proposalAccountNetwork - val foundNetwork = if (portfolioAccountId != null) { - requireNotNull(proposalAccountNetwork)[portfolioAccountId] - } else { - sessionProposal.proposalNetwork[selectedUserWalletFlow.value] - } + val foundNetwork = proposalAccountNetwork[portfolioAccountId] if (foundNetwork == null) { processError(Unknown("Selected wallet not found")) } else { - val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios) - if (proposalAccountNetwork != null) { - selectorController.isEnabled.value = { wallet, account -> - proposalAccountNetwork.contains(account.account.accountId) - } + val portfolioSelectRow = createPortfolioSelectRow( + selectedPortfolio = selected, + portfolios = portfolios, + isAccountMode = isAccountMode, + ) + selectorController.isEnabled.value = { _, account -> + proposalAccountNetwork.contains(account.account.accountId) } proposalNetwork = foundNetwork additionallyEnabledNetworks = proposalNetwork.available @@ -238,13 +211,6 @@ internal class WcPairModel @Inject constructor( onDismiss = ::rejectPairing, onConnect = ::onConnect, portfolioSelectRow = portfolioSelectRow, - onWalletClick = { - stackNavigation.pushNew( - WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId), - ) - }.takeIf { - portfolioSelectRow == null && availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT - }, onNetworksClick = { stackNavigation.pushNew( WcAppInfoRoutes.SelectNetworks( @@ -256,7 +222,6 @@ internal class WcPairModel @Inject constructor( ), ) }, - userWallet = portfolioWallet ?: selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, additionallyEnabledNetworks = additionallyEnabledNetworks, ), @@ -264,17 +229,15 @@ internal class WcPairModel @Inject constructor( } } - private suspend fun tryToCreatePortfolioSelectRow( - selectedPortfolio: Pair?, - portfolios: PortfolioFetcher.Data?, - ): PortfolioSelectUM? { - selectedPortfolio ?: return null - portfolios ?: return null + private suspend fun createPortfolioSelectRow( + selectedPortfolio: Pair, + portfolios: PortfolioFetcher.Data, + isAccountMode: Boolean, + ): PortfolioSelectUM { val (wallet, portfolioAccount) = selectedPortfolio val account = when (val account = portfolioAccount.account) { is Account.CryptoPortfolio -> account } - val isAccountMode = selectorController.isAccountMode.first() val icon: AccountIconUM.CryptoPortfolio? val name: TextReference if (isAccountMode) { @@ -317,15 +280,16 @@ internal class WcPairModel @Inject constructor( private fun connect() { val enabledAvailableNetworks = proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks } - val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull() - val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value - val account = selectedPortfolio?.second?.account + val selectedPortfolio = selectedPortfolio.replayCache + .firstOrNull() + ?: return + val (wallet, account) = selectedPortfolio modelScope.launch { analytics.send( WcAnalyticEvents.PairButtonConnect( dAppName = sessionProposal.dAppMetaData.name, - accountDerivation = account?.derivationIndex?.value, + accountDerivation = account.account.derivationIndex.value, ), ) } @@ -333,7 +297,7 @@ internal class WcPairModel @Inject constructor( WcSessionApprove( wallet = wallet, network = enabledAvailableNetworks + proposalNetwork.required, - account = account, + account = account.account, ), ) } @@ -389,20 +353,6 @@ internal class WcPairModel @Inject constructor( alert?.let { stackNavigation.pushNew(it) } } - override fun onWalletSelected(userWalletId: UserWalletId) { - val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } - proposalNetwork = sessionProposal.proposalNetwork[selectedUserWallet] ?: return - selectedUserWalletFlow.update { selectedUserWallet } - additionallyEnabledNetworks = proposalNetwork.available - appInfoUiState.transformerUpdate( - WcAppInfoWalletChangedTransformer( - selectedUserWallet = selectedUserWallet, - proposalNetwork = proposalNetwork, - additionallyEnabledNetworks = additionallyEnabledNetworks, - ), - ) - } - override fun onNetworksSelected(selectedNetworks: Set) { additionallyEnabledNetworks = selectedNetworks appInfoUiState.transformerUpdate( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt deleted file mode 100644 index e9f6c92a3b..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.walletconnect.connections.model - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.wallet.utils.UserWalletsFetcher -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.WcSelectWalletParams -import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM -import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -@Suppress("LongParameterList") -@Stable -@ModelScoped -internal class WcSelectWalletModel @Inject constructor( - paramsContainer: ParamsContainer, - messageSender: UiMessageSender, - userWalletsFetcherFactory: UserWalletsFetcher.Factory, - getTokenListUseCase: GetTokenListUseCase, - private val router: Router, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - internal val state: StateFlow - field = MutableStateFlow( - WcAppInfoWalletUM( - wallets = persistentListOf(), - selectedUserWalletId = params.selectedWalletId, - ), - ) - - private val userWalletsFetcher = WcUserWalletsFetcher( - userWalletsFetcherFactory = userWalletsFetcherFactory, - getTokenListUseCase = getTokenListUseCase, - messageSender = messageSender, - onWalletSelected = ::onWalletSelected, - ) - - init { - userWalletsFetcher - .userWallets - .onEach { state.update { state -> state.copy(wallets = it) } } - .launchIn(modelScope) - } - - private fun onWalletSelected(userWalletId: UserWalletId) { - params.callback.onWalletSelected(userWalletId) - router.pop() - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index d6099eaaa2..e8597c20e5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM @@ -16,10 +15,8 @@ internal class WcAppInfoTransformer( private val dAppVerifiedStateConverter: WcDAppVerifiedStateConverter, private val onDismiss: () -> Unit, private val onConnect: (securityStatus: CheckDAppResult) -> Unit, - private val portfolioSelectRow: PortfolioSelectUM?, - private val onWalletClick: (() -> Unit)?, + private val portfolioSelectRow: PortfolioSelectUM, private val onNetworksClick: () -> Unit, - private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, private val additionallyEnabledNetworks: Set, ) : Transformer { @@ -33,8 +30,6 @@ internal class WcAppInfoTransformer( ), appSubtitle = WcAppSubtitleConverter.convert(dAppSession.dAppMetaData), notification = createNotification(dAppSession.securityStatus), - walletName = userWallet.name, - onWalletClick = onWalletClick, portfolioSelectRow = portfolioSelectRow, networksInfo = WcNetworksInfoConverter.convert( value = WcNetworksInfoConverter.Input( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt deleted file mode 100644 index 54d753bd72..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.walletconnect.connections.model.transformers - -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.walletconnect.model.WcSessionProposal -import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM -import com.tangem.utils.transformer.Transformer - -internal class WcAppInfoWalletChangedTransformer( - private val selectedUserWallet: UserWallet, - private val proposalNetwork: WcSessionProposal.ProposalNetwork, - private val additionallyEnabledNetworks: Set, -) : Transformer { - override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { - val contentState = prevState as? WcAppInfoUM.Content ?: return prevState - return contentState.copy( - walletName = selectedUserWallet.name, - networksInfo = WcNetworksInfoConverter.convert( - WcNetworksInfoConverter.Input( - missingNetworks = proposalNetwork.missingRequired, - requiredNetworks = proposalNetwork.required, - availableNetworks = proposalNetwork.available, - notAddedNetworks = proposalNetwork.notAdded, - additionallyEnabledNetworks = additionallyEnabledNetworks, - ), - ), - connectButtonConfig = prevState.connectButtonConfig.copy( - enabled = WcConnectButtonAvailabilityConverter.convert( - WcConnectButtonAvailabilityConverter.Input( - missingNetworks = proposalNetwork.missingRequired, - requiredNetworks = proposalNetwork.required, - availableNetworks = proposalNetwork.available, - selectedNetworks = additionallyEnabledNetworks, - ), - ), - ), - ) - } -} \ No newline at end of file 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 f3991ee130..7ff2f97a29 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 @@ -49,7 +49,7 @@ internal class WcSessionsAccountModeTransformer( items.add(walletHeader) accountList.accounts.filterIsInstance().forEach accountsForEach@{ account -> - val accountSessions = sessions.filter { it.account?.accountId == account.accountId } + val accountSessions = sessions.filter { it.account.accountId == account.accountId } if (accountSessions.isEmpty()) return@accountsForEach val connectedApps = accountSessions.map { dappSession -> with(dappSession.sdkModel) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index e2a866155b..63edee72e6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.routes import androidx.compose.runtime.Immutable import com.tangem.core.decompose.navigation.Route import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @Serializable @@ -15,9 +14,6 @@ internal sealed class WcAppInfoRoutes : Route { @Serializable data object PortfolioSelector : WcAppInfoRoutes() - @Serializable - data class SelectWallet(val selectedWalletId: UserWalletId) : WcAppInfoRoutes() - @Serializable data class SelectNetworks( val missingRequiredNetworks: Set, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 9bfbb0ec01..ad8dad09c5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -22,8 +22,6 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag 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.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -273,19 +271,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier val itemsModifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12) - if (state.portfolioSelectRow != null) { - PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow) - } else { - WalletRowItem( - modifier = if (state.onWalletClick != null) { - Modifier.clickableSingle(onClick = state.onWalletClick) - } else { - Modifier - }.then(itemsModifier), - walletName = state.walletName, - showEndIcon = state.onWalletClick != null, - ) - } + PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( modifier = Modifier @@ -341,56 +327,6 @@ private fun PortfolioRowItem(portfolioSelectRow: PortfolioSelectUM, modifier: Mo } } -@Composable -private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Icon( - modifier = Modifier - .size(24.dp) - .testTag(WalletConnectBottomSheetTestTags.WALLET_ICON), - painter = painterResource(R.drawable.ic_wallet_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing4) - .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE), - text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - ) - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing16) - .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME), - text = walletName, - textAlign = TextAlign.End, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - if (showEndIcon) { - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing12) - .size(width = 18.dp, height = 24.dp), - painter = painterResource(R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - @Composable private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier = Modifier) { Row( @@ -673,9 +609,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Unit, -) { - - private val userWalletsFetcher = userWalletsFetcherFactory.create( - messageSender = messageSender, - onlyMultiCurrency = true, - isAuthMode = false, - isClickableIfLocked = false, - onWalletClick = { onWalletSelected(it) }, - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val userWallets: Flow> = userWalletsFetcher.userWallets - .flatMapLatest { listOfWalletItem -> - val flows = listOfWalletItem.map(::getTokenListFlow) - combine(flows) { it.toList().toImmutableList() } - } - - private fun getTokenListFlow(walletItem: UserWalletItemUM): Flow { - return getTokenListUseCase.launch(UserWalletId(walletItem.id)).map { lce -> - val information = lce.fold( - ifLoading = { UserWalletItemUM.Information.Loading }, - ifError = { UserWalletItemUM.Information.Failed }, - ifContent = { tokenList -> tokenCountInfo(tokenList.flattenCurrencies().size) }, - ) - walletItem.copy(information = information) - } - } - - private fun tokenCountInfo(count: Int): UserWalletItemUM.Information.Loaded { - val text = TextReference.PluralRes( - id = R.plurals.card_label_token_count, - count = count, - formatArgs = wrappedList(count), - ) - return UserWalletItemUM.Information.Loaded(text) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt index f40742eb25..620599339d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -28,11 +28,6 @@ internal interface WalletConnectModelModule { @ClassKey(WcPairModel::class) fun bindWcPairModel(model: WcPairModel): Model - @Binds - @IntoMap - @ClassKey(WcSelectWalletModel::class) - fun bindWcSelectWalletModel(model: WcSelectWalletModel): Model - @Binds @IntoMap @ClassKey(WcSelectNetworksModel::class) 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 11fba2839f..78f0d51df5 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 @@ -137,7 +137,7 @@ internal class WcAddNetworkModel @Inject constructor( network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), ) 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 a156c5a69c..493b5158d4 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 @@ -426,7 +426,7 @@ internal class WcSendTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = securityStatusState.value.toCheckDAppResult(), - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ) analytics.send(event) showSuccessSignMessage() @@ -466,7 +466,7 @@ internal class WcSendTransactionModel @Inject constructor( network = useCase.network, emulationStatus = emulationStatus, securityStatus = securityCheck.toCheckDAppResult(), - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), ) 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 7a0a21fe7f..2f36707a60 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 @@ -143,7 +143,7 @@ internal class WcSignTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ) analytics.send(event) showSuccessSignMessage() @@ -172,7 +172,7 @@ internal class WcSignTransactionModel @Inject constructor( network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), ) diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt deleted file mode 100644 index 37c2e71379..0000000000 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.yield.supply.api - -interface YieldSupplyFeatureToggles { - - val isYieldSupplyFeatureEnabled: Boolean - val isYieldSupplyPendingTransactionsEnabled: Boolean -} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt deleted file mode 100644 index 4f0a442329..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.yield.supply.impl - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles - -internal class DefaultYieldSupplyFeatureToggles( - private val featureToggles: FeatureTogglesManager, -) : YieldSupplyFeatureToggles { - override val isYieldSupplyFeatureEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED") - - override val isYieldSupplyPendingTransactionsEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED") -} \ No newline at end of file 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 e5f5e68cca..cec6ab682a 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 @@ -1,7 +1,6 @@ package com.tangem.features.yield.supply.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -24,6 +23,7 @@ class YieldSupplyAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -44,35 +44,17 @@ class YieldSupplyAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick(userWallet: UserWallet, cryptoCurrency: CryptoCurrency?, errorMessage: String?) { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt deleted file mode 100644 index 45b8806bd6..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.yield.supply.impl.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@InstallIn(SingletonComponent::class) -@Module -internal object YieldSupplyFeatureModule { - - @Singleton - @Provides - fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles { - return DefaultYieldSupplyFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4c87fdffe7..759030eecc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -102,6 +102,9 @@ markdownComposeView = "0.5.4" usedesk = "4.4.0" sumsub = "1.38.0" haze = "1.7.1" +kotlinpoet = "1.18.1" +customerio = "4.6.3" +surveysparrow = "1.2.9" # endregion Other libraries # region Tools @@ -149,6 +152,7 @@ agconnect = { id = "com.huawei.agconnect", version.ref = "agconnect" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradle-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } # end region Classpath # region AndroidX @@ -311,4 +315,7 @@ usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } +customerio-analytics = { module = "io.customer.android:datapipelines", version.ref = "customerio" } +customerio-messaging = { module = "io.customer.android:messaging-push-fcm", version.ref = "customerio" } +surveysparrow = { module = "com.github.surveysparrow:surveysparrow-android-sdk", version.ref = "surveysparrow" } # endregion Other diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index bcb442df0a..b6a00beda3 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "releases-5.35-1449" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-591" +tangemCardSdk = "releases-5.35-593" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index f4ca59e975..a812c22529 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -1,8 +1,8 @@ package com.tangem.blockchainsdk +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -15,16 +15,16 @@ internal typealias BlockchainProvidersResponse = Map /** * Implementation of Blockchain SDK components factory * + * @property blockchainSdkConfig blockchain SDK config * @property blockchainProvidersTypesManager blockchain providers types manager - * @property environmentConfigStorage environment config storage * @property walletManagerFactoryCreator wallet manager factory creator * @param dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultBlockchainSDKFactory( + private val blockchainSdkConfig: BlockchainSdkConfig, private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - private val environmentConfigStorage: EnvironmentConfigStorage, private val walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKFactory { @@ -43,7 +43,7 @@ internal class DefaultBlockchainSDKFactory( private fun createWalletManagerFactory(): Flow { return combine( - flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig }, + flow = flowOf(blockchainSdkConfig), flow2 = blockchainProvidersTypesManager.get(), // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index a4f5188846..6c0fb03aca 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,7 +7,6 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import timber.log.Timber import javax.inject.Inject @@ -24,7 +23,6 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val featureTogglesManager: FeatureTogglesManager, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -35,10 +33,8 @@ internal class WalletManagerFactoryCreator @Inject constructor( blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( - isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"), - isPendingTransactionsEnabled = featureTogglesManager.isFeatureEnabled( - "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - ), + isYieldSupplyEnabled = true, + isPendingTransactionsEnabled = true, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 9abd481655..a8c5f4b3f5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -17,10 +17,9 @@ import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.libs.blockchain_sdk.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,14 +39,14 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainSDKFactory( + environmentConfig: EnvironmentConfig, blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - environmentConfigStorage: EnvironmentConfigStorage, walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ): BlockchainSDKFactory { return DefaultBlockchainSDKFactory( + blockchainSdkConfig = environmentConfig.blockchainSdkConfig, blockchainProvidersTypesManager = blockchainProvidersTypesManager, - environmentConfigStorage = environmentConfigStorage, walletManagerFactoryCreator = walletManagerFactoryCreator, dispatchers = dispatchers, ) @@ -91,13 +90,11 @@ internal object BlockchainSDKFactoryModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, blockchainSDKLogger: BlockchainSDKLogger, - featureTogglesManager: FeatureTogglesManager, ): WalletManagerFactoryCreator { return WalletManagerFactoryCreator( accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - featureTogglesManager = featureTogglesManager, ) } } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 81838cfdbb..1be48e89c6 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -85,11 +85,20 @@ object BlockchainUtils { excludedBlockchains: ExcludedBlockchains, hotExcludedBlockchains: Set, hasOnlyHotWallets: Boolean = false, + coinId: String? = null, + contractAddress: String? = null, ): Boolean { - val blockchain = Blockchain.fromNetworkId(blockchainId) + val blockchain = Blockchain.fromNetworkId(blockchainId) ?: return false - return blockchain != null && blockchain !in excludedBlockchains && - (hasOnlyHotWallets.not() || blockchain !in hotExcludedBlockchains) + if (blockchain in excludedBlockchains) return false + if (hasOnlyHotWallets && blockchain in hotExcludedBlockchains) return false + + if (!contractAddress.isNullOrEmpty()) { + if (!blockchain.canHandleTokens()) return false + if (coinId != null && !isNotBlockedByTerraV1Filter(blockchainId, coinId)) return false + } + + return true } fun isArbitrum(blockchainId: String): Boolean { diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index a8bc7da8a0..4d9ccbce70 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -17,6 +17,15 @@ dependencies { implementation(deps.gradle.kotlin) implementation(deps.gradle.android) implementation(deps.gradle.detekt) + implementation(deps.gradle.kotlinpoet) + implementation(deps.kotlin.serialization) + + testImplementation(deps.test.junit5) + testImplementation(deps.test.truth) +} + +tasks.withType { + useJUnitPlatform() } gradlePlugin { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt new file mode 100644 index 0000000000..93e83b4e87 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -0,0 +1,196 @@ +package com.tangem.plugin.configuration.configurations + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlinx.serialization.json.* +import java.io.File +import java.util.Locale + +/** + * Generator for environment configuration Kotlin object from JSON file. + * Automatically parses JSON structure and generates corresponding Kotlin code. + * +[REDACTED_AUTHOR] + */ +object EnvironmentConfigGenerator { + + private const val PACKAGE_NAME = "com.tangem.datasource.local.config.environment.generated" + private const val CLASS_NAME = "GeneratedEnvironmentConfig" + + /** + * Generates GeneratedEnvironmentConfig object from JSON file. + * + * @param inputFile JSON configuration file + * @param outputDir Output directory for generated Kotlin file + */ + fun generate(inputFile: File, outputDir: File) { + val jsonText = inputFile.readText() + val json = Json.parseToJsonElement(jsonText).jsonObject + + val objectBuilder = TypeSpec.objectBuilder(CLASS_NAME) + .addKdoc("Generated from ${inputFile.name}\nAuto-generated - do not edit manually.") + + // Iterate over all JSON keys and generate properties + json.entries.forEach { (key, value) -> + addPropertyFromJsonValue(objectBuilder, key, value) + } + + val fileSpec = FileSpec.builder(PACKAGE_NAME, CLASS_NAME) + .indent(" ") // Use 4 spaces for indentation + .addType(objectBuilder.build()) + .build() + + outputDir.mkdirs() + fileSpec.writeTo(outputDir) + + // Post-process generated file + val generatedFile = File(outputDir, PACKAGE_NAME.replace('.', '/') + "/$CLASS_NAME.kt") + if (generatedFile.exists()) { + val content = generatedFile.readText() + val fixedContent = content + // Add suppress annotation at file level + .replaceFirst( + "package $PACKAGE_NAME", + "@file:Suppress(\n" + + " \"MaximumLineLength\",\n" + + " \"MaxLineLength\",\n" + + " \"Indentation\",\n" + + ")\n\npackage $PACKAGE_NAME" + ) + // Remove redundant public modifiers + .replace("public object ", "object ") + .replace("public val ", "val ") + .replace("public const val ", "const val ") + generatedFile.writeText(fixedContent) + } + } + + /** + * Adds a property to the TypeSpec based on the JSON value type + */ + private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { + val propertyName = name.toValidIdentifier() + when (value) { + is JsonPrimitive -> { + when { + value.isString -> { + val stringValue = value.content + val propertySpec = PropertySpec.builder(propertyName, STRING) + .addModifiers(KModifier.CONST) + .initializer("%S", stringValue) + + builder.addProperty(propertySpec.build()) + } + value.booleanOrNull != null -> { + builder.addProperty( + PropertySpec.builder(propertyName, BOOLEAN) + .addModifiers(KModifier.CONST) + .initializer("%L", value.boolean) + .build() + ) + } + value.longOrNull != null -> { + builder.addProperty( + PropertySpec.builder(propertyName, LONG) + .addModifiers(KModifier.CONST) + .initializer("%L", value.long) + .build() + ) + } + value.doubleOrNull != null -> { + builder.addProperty( + PropertySpec.builder(propertyName, DOUBLE) + .addModifiers(KModifier.CONST) + .initializer("%L", value.double) + .build() + ) + } + else -> { + // Null value + builder.addProperty( + PropertySpec.builder(propertyName, STRING.copy(nullable = true)) + .initializer("null") + .build() + ) + } + } + } + is JsonArray -> { + val listType = LIST.parameterizedBy(STRING) + val values = value.map { it.jsonPrimitive.content } + builder.addProperty( + PropertySpec.builder(propertyName, listType) + .initializer( + CodeBlock.builder() + .add("listOf(\n") + .apply { + values.forEach { v -> + add(" %S,\n", v) + } + } + .add(")") + .build() + ) + .build() + ) + } + is JsonObject -> { + // Generate nested object with proper naming (convert dashes to camelCase) + val nestedClassName = name.toPascalCase() + val nestedObjectBuilder = TypeSpec.objectBuilder(nestedClassName) + + value.entries.forEach { (nestedKey, nestedValue) -> + addPropertyFromJsonValue(nestedObjectBuilder, nestedKey, nestedValue) + } + + builder.addType(nestedObjectBuilder.build()) + } + } + } + + /** + * Converts a string to PascalCase for use as a class/object name. + * - If the string contains separators (dots, dashes, underscores), splits and joins in PascalCase + * - If no separators, just capitalizes the first letter to preserve original casing (e.g., "AppsFlyer" stays "AppsFlyer") + */ + private fun String.toPascalCase(): String { + val hasSeparators = contains('.') || contains('-') || contains('_') + return if (hasSeparators) { + this.split("-", "_", ".") + .filter { it.isNotEmpty() } + .joinToString("") { part -> + part.lowercase(Locale.ROOT).replaceFirstChar { it.uppercase(Locale.ROOT) } + } + } else { + this.replaceFirstChar { it.uppercase(Locale.ROOT) } + } + } + + /** + * Converts a string to a valid Kotlin property identifier. + * - If the string contains dots, converts to camelCase (dots cannot be escaped by KotlinPoet) + * - Otherwise, ensures the first letter is lowercase (Kotlin property naming convention) + */ + private fun String.toValidIdentifier(): String { + return if (contains('.')) { + toCamelCase() + } else { + this.replaceFirstChar { it.lowercase(Locale.ROOT) } + } + } + + /** + * Converts a string to camelCase, handling dashes, underscores, and dots. + * Normalizes each segment to lowercase first for consistent results. + * Examples: "cosmos-hub" -> "cosmosHub", "customer.io" -> "customerIo", "CUSTOMER.IO" -> "customerIo" + */ + private fun String.toCamelCase(): String { + val parts = this.split("-", "_", ".") + .filter { it.isNotEmpty() } + return parts.mapIndexed { index, part -> + val normalized = part.lowercase(Locale.ROOT) + if (index == 0) normalized + else normalized.replaceFirstChar { it.uppercase(Locale.ROOT) } + }.joinToString("") + } +} diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt index 644b1eaead..20b67b7b6d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt @@ -5,4 +5,5 @@ import org.gradle.api.Project internal fun Project.configure() { configureKotlinCompilerOptions() configureDetektRules() + configureTestLogging() } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt new file mode 100644 index 0000000000..5f5c3d5d9c --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt @@ -0,0 +1,48 @@ +package com.tangem.plugin.configuration.configurations + +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.TestDescriptor +import org.gradle.api.tasks.testing.TestListener +import org.gradle.api.tasks.testing.TestResult +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import java.io.Serializable + +internal fun Project.configureTestLogging() { + tasks.withType(Test::class.java).configureEach { + println("Test task scheduled: $path") + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + addTestListener(TestSuiteLogger(path)) + } +} + +private class TestSuiteLogger(private val taskPath: String) : TestListener, Serializable { + override fun beforeSuite(suite: TestDescriptor) {} + + override fun afterSuite(suite: TestDescriptor, result: TestResult) { + if (suite.parent == null) { + val output = + "$taskPath - Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + val startItem = "| " + val endItem = " |" + val repeatLength = startItem.length + output.length + endItem.length + println( + "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( + repeatLength, + ), + ) + } + } + + override fun beforeTest(testDescriptor: TestDescriptor) {} + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {} + + companion object { + private const val serialVersionUID = 1L + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index e60739fd69..96995ee158 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -24,6 +24,8 @@ private fun AppExtension.configureDefaultConfig(project: Project) { minSdk = AppConfig.minSdkVersion targetSdk = AppConfig.targetSdkVersion + ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a") + versionCode = if (project.hasProperty("versionCode")) { (project.property("versionCode") as String).toInt() } else { @@ -70,6 +72,7 @@ private fun AppExtension.configureBuildTypes() { } private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, buildType: BuildType) { + val x86_64 = "x86_64" when (buildType) { BuildType.Release -> { isDebuggable = false @@ -77,6 +80,7 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b BuildType.Debug -> { isDebuggable = true signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) + ndk.abiFilters += x86_64 } BuildType.Internal, BuildType.External @@ -84,12 +88,14 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) + ndk.abiFilters += x86_64 } BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) isDebuggable = true + ndk.abiFilters += x86_64 } } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 0ab5e8f747..c15761c0d9 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -1,11 +1,11 @@ package com.tangem.plugin.configuration.model -internal enum class BuildType( +enum class BuildType( val id: String, - val appIdSuffix: String? = null, - val versionSuffix: String? = null, - val obfuscating: Boolean = false, - val configFields: List, + internal val appIdSuffix: String? = null, + internal val versionSuffix: String? = null, + internal val obfuscating: Boolean = false, + internal val configFields: List, ) { /** @@ -117,4 +117,19 @@ internal enum class BuildType( BuildConfigField.ABTestsEnabled(isEnabled = false), ), ), + ; + + /** Returns the environment value (dev/prod) for this build type */ + val environment: String + get() { + val environmentField = configFields + .filterIsInstance() + .firstOrNull() + + requireNotNull(environmentField) { + "BuildType '$id' must have a BuildConfigField.Environment in configFields" + } + + return environmentField.value.removeSurrounding("\"") + } } \ No newline at end of file diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt new file mode 100644 index 0000000000..dae3182b9d --- /dev/null +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt @@ -0,0 +1,561 @@ +package com.tangem.plugin.configuration.configurations + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +/** + * Tests for [EnvironmentConfigGenerator] covering JSON parsing edge cases. + */ +class EnvironmentConfigGeneratorTest { + + @TempDir + lateinit var tempDir: File + + private lateinit var outputDir: File + + @BeforeEach + fun setup() { + outputDir = File(tempDir, "output") + } + + @Test + fun `generate handles string values correctly`() { + // Arrange + val json = """ + { + "apiKey": "test-api-key", + "baseUrl": "https://example.com" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("""const val apiKey: String = "test-api-key"""") + assertThat(generatedCode).contains("""const val baseUrl: String = "https://example.com"""") + } + + @Test + fun `generate handles empty string as nullable`() { + // Arrange + val json = """ + { + "emptyValue": "" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyValue: String? = null") + } + + @Test + fun `generate handles null values`() { + // Arrange + val json = """ + { + "nullValue": null + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val nullValue: String? = null") + } + + @Test + fun `generate handles boolean values`() { + // Arrange + val json = """ + { + "isEnabled": true, + "isDisabled": false + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val isEnabled: Boolean = true") + assertThat(generatedCode).contains("const val isDisabled: Boolean = false") + } + + @Test + fun `generate handles integer values as Long`() { + // Arrange + val json = """ + { + "count": 42, + "negativeNumber": -100, + "largeNumber": 9223372036854775807 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val count: Long = 42") + assertThat(generatedCode).contains("const val negativeNumber: Long = -100") + // KotlinPoet formats large numbers with underscores + assertThat(generatedCode).contains("const val largeNumber: Long = 9_223_372_036_854_775_807") + } + + @Test + fun `generate handles double values`() { + // Arrange + val json = """ + { + "ratio": 3.14, + "negativeDouble": -2.5 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val ratio: Double = 3.14") + assertThat(generatedCode).contains("const val negativeDouble: Double = -2.5") + } + + @Test + fun `generate handles string arrays`() { + // Arrange + val json = """ + { + "items": ["one", "two", "three"] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val items: List = listOf(") + assertThat(generatedCode).contains(""""one",""") + assertThat(generatedCode).contains(""""two",""") + assertThat(generatedCode).contains(""""three",""") + } + + @Test + fun `generate handles empty arrays`() { + // Arrange + val json = """ + { + "emptyList": [] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyList: List = listOf(") + } + + @Test + fun `generate handles nested objects`() { + // Arrange + val json = """ + { + "database": { + "host": "localhost", + "port": 5432 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Database {") + assertThat(generatedCode).contains("""const val host: String = "localhost"""") + // KotlinPoet formats numbers >= 1000 with underscores + assertThat(generatedCode).contains("const val port: Long = 5_432") + } + + @Test + fun `generate handles deeply nested objects`() { + // Arrange + val json = """ + { + "level1": { + "level2": { + "level3": { + "deepValue": "deep" + } + } + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Level1 {") + assertThat(generatedCode).contains("object Level2 {") + assertThat(generatedCode).contains("object Level3 {") + assertThat(generatedCode).contains("""const val deepValue: String = "deep"""") + } + + @Test + fun `generate preserves camelCase object names without separators`() { + // Arrange - object names like "AppsFlyer" should stay as "AppsFlyer", not become "Appsflyer" + val json = """ + { + "AppsFlyer": { + "DevKey": "key123" + }, + "GetBlockAccessTokens": { + "ethereum": { + "jsonRpc": "token" + } + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - object names preserved, property names have first letter lowercased + assertThat(generatedCode).contains("object AppsFlyer {") + assertThat(generatedCode).contains("""const val devKey: String = "key123"""") + assertThat(generatedCode).contains("object GetBlockAccessTokens {") + assertThat(generatedCode).contains("object Ethereum {") + } + + @Test + fun `generate converts dash-separated names to PascalCase`() { + // Arrange + val json = """ + { + "cosmos-hub": { + "chainId": "cosmoshub-4" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate converts underscore-separated names to PascalCase`() { + // Arrange + val json = """ + { + "api_config": { + "timeout": 30 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object ApiConfig {") + } + + @Test + fun `generate handles consecutive dashes in names`() { + // Arrange + val json = """ + { + "cosmos--hub": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate handles trailing dash in names`() { + // Arrange + val json = """ + { + "config-": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Config {") + } + + @Test + fun `generate handles special characters in string values`() { + // Arrange + val json = """ + { + "query": "SELECT * FROM users WHERE name = 'John'", + "path": "C:\\Users\\test", + "newline": "line1\nline2", + "unicode": "Hello 世界" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val query: String") + assertThat(generatedCode).contains("const val path: String") + assertThat(generatedCode).contains("const val newline: String") + assertThat(generatedCode).contains("const val unicode: String") + } + + @Test + fun `generate handles arrays with special characters`() { + // Arrange + val json = """ + { + "urls": [ + "https://api.example.com/v1", + "https://api.example.com/v2?key=value&other=1" + ] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val urls: List = listOf(") + assertThat(generatedCode).contains(""""https://api.example.com/v1",""") + } + + @Test + fun `generate adds file suppress annotations`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("@file:Suppress(") + assertThat(generatedCode).contains(""""MaximumLineLength"""") + assertThat(generatedCode).contains(""""MaxLineLength"""") + assertThat(generatedCode).contains(""""Indentation"""") + } + + @Test + fun `generate creates proper package declaration`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("package com.tangem.datasource.local.config.environment.generated") + } + + @Test + fun `generate creates object with correct name`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object GeneratedEnvironmentConfig {") + } + + @Test + fun `generate adds kdoc with source file reference`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("Generated from") + assertThat(generatedCode).contains("Auto-generated - do not edit manually") + } + + @Test + fun `generate removes public modifiers`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).doesNotContain("public object") + assertThat(generatedCode).doesNotContain("public val") + assertThat(generatedCode).doesNotContain("public const val") + } + + @Test + fun `generate handles complex real-world config`() { + // Arrange + val json = """ + { + "tangemComApiKey": "api-key-123", + "moonPayApiKey": "moon-pay-key", + "moonPayApiSecretKey": "secret-key", + "mercuryoWidgetId": "", + "blockchainSdkConfig": { + "blockchairApiKey": "blockchair-key", + "blockcypherTokens": ["token1", "token2"], + "quickNodeSolanaCredentials": { + "apiKey": "solana-key", + "subdomain": "solana-node" + } + }, + "isFeatureEnabled": true, + "maxRetryCount": 3 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + // Top-level properties + assertThat(generatedCode).contains("""const val tangemComApiKey: String = "api-key-123"""") + assertThat(generatedCode).contains("val mercuryoWidgetId: String? = null") + assertThat(generatedCode).contains("const val isFeatureEnabled: Boolean = true") + assertThat(generatedCode).contains("const val maxRetryCount: Long = 3") + + // Nested object + assertThat(generatedCode).contains("object BlockchainSdkConfig {") + assertThat(generatedCode).contains("""const val blockchairApiKey: String = "blockchair-key"""") + assertThat(generatedCode).contains("val blockcypherTokens: List") + + // Deeply nested object + assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {") + } + + @Test + fun `generate converts dot-separated object names to PascalCase`() { + // Arrange - testing the customer.io case that caused the original build failure + val json = """ + { + "customer.io": { + "TrackSiteID": "site-id-123", + "TrackApiKey": "api-key-456" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CustomerIo {") + // Property names have first letter lowercased (Kotlin convention) + assertThat(generatedCode).contains("""const val trackSiteID: String = "site-id-123"""") + assertThat(generatedCode).contains("""const val trackApiKey: String = "api-key-456"""") + } + + @Test + fun `generate converts dot-separated property names to camelCase`() { + // Arrange - testing property names with dots (not nested objects) + val json = """ + { + "api.key": "test-key", + "service.url": "https://example.com" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - dots in property names are converted to camelCase + assertThat(generatedCode).contains("""const val apiKey: String = "test-key"""") + assertThat(generatedCode).contains("""const val serviceUrl: String = "https://example.com"""") + } + + @Test + fun `generate preserves valid property names without transformation`() { + // Arrange - valid Kotlin identifiers should not be transformed + val json = """ + { + "apiKey": "key1", + "moonPayApiKey": "moon-pay-key", + "isEnabled": true, + "maxRetryCount": 5 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - original names preserved exactly + assertThat(generatedCode).contains("""const val apiKey: String = "key1"""") + assertThat(generatedCode).contains("""const val moonPayApiKey: String = "moon-pay-key"""") + assertThat(generatedCode).contains("const val isEnabled: Boolean = true") + assertThat(generatedCode).contains("const val maxRetryCount: Long = 5") + } + + private fun generateAndReadOutput(jsonContent: String): String { + val inputFile = File(tempDir, "config.json").apply { + writeText(jsonContent) + } + + EnvironmentConfigGenerator.generate(inputFile, outputDir) + + val generatedFile = File( + outputDir, + "com/tangem/datasource/local/config/environment/generated/GeneratedEnvironmentConfig.kt" + ) + + assertThat(generatedFile.exists()).isTrue() + return generatedFile.readText() + } +} + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index d71fbf5bea..f77aab113a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:token-recieve:impl") include(":features:yield-supply:api") include(":features:yield-supply:impl") +include(":features:approval:api") +include(":features:approval:impl") + include(":features:feed:api") include(":features:feed:impl") // endregion Feature modules @@ -349,6 +352,7 @@ include(":domain:manage-tokens") include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") +include(":domain:offramp") include(":domain:promo") include(":domain:promo:models") include(":domain:nft") diff --git a/test/core/build.gradle.kts b/test/core/build.gradle.kts index d34ffdd2aa..ad834bcedb 100644 --- a/test/core/build.gradle.kts +++ b/test/core/build.gradle.kts @@ -10,4 +10,5 @@ dependencies { api(deps.test.junit5) api(deps.test.mockk) api(deps.test.truth) + api(deps.test.turbine) } \ No newline at end of file