diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt new file mode 100644 index 0000000000..f4648d1cc9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt @@ -0,0 +1,20 @@ +package com.tangem.common.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context + +fun getClipboardText(context: Context): String? { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + return if (clipboard.hasPrimaryClip()) { + clipboard.primaryClip?.getItemAt(0)?.text?.toString() + } else { + null + } +} + +fun setClipboardText(context: Context, text: String?) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("label", text) + clipboard.setPrimaryClip(clip) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 3212a90432..d07178f3cb 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -85,4 +85,13 @@ fun BaseTestCase.openDeviceSettingsScreen() { step("Click on 'Device settings' button") { onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() } } +} + +fun BaseTestCase.openWalletConnectScreen() { + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click on 'Wallet Connect' button") { + onDetailsScreen { walletConnectButton.clickWithAssertion() } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt index 107806004a..facffbfd2d 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt @@ -7,6 +7,7 @@ import com.tangem.screens.onWalletConnectScreen import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.checkWalletConnectBottomSheet() { + waitForIdle() step("Assert 'Wallet Connect' bottom sheet title is displayed") { onWalletConnectBottomSheet { title.assertIsDisplayed() } } @@ -60,34 +61,64 @@ fun BaseTestCase.checkWalletConnectBottomSheet() { } } -fun BaseTestCase.checkWalletConnectScreen() { +fun BaseTestCase.checkWalletConnectScreen(withConnections: Boolean) { + waitForIdle() step("Assert 'Wallet Connect' title is displayed") { onWalletConnectScreen { title.assertIsDisplayed() } } - step("Assert 'More' button is displayed") { - onWalletConnectScreen { moreButton.assertIsDisplayed() } - } - step("Assert wallet name is displayed") { - onWalletConnectScreen { walletName.assertIsDisplayed() } - } - step("Assert app icon is displayed") { - onWalletConnectScreen { appIcon.assertIsDisplayed() } - } - step("Assert app name is displayed") { - onWalletConnectScreen { appName.assertIsDisplayed() } - } - step("Assert approve icon is displayed") { - onWalletConnectScreen { approveIcon.assertIsDisplayed() } - } - step("Assert app URL is displayed") { - onWalletConnectScreen { appUrl.assertIsDisplayed() } - } step("Assert 'New Connection' button is displayed") { onWalletConnectScreen { newConnectionButton.assertIsDisplayed() } } + if (withConnections) { + step("Assert 'More' button is displayed") { + onWalletConnectScreen { moreButton.assertIsDisplayed() } + } + step("Assert wallet name is displayed") { + onWalletConnectScreen { walletName.assertIsDisplayed() } + } + step("Assert app icon is displayed") { + onWalletConnectScreen { appIcon.assertIsDisplayed() } + } + step("Assert app name is displayed") { + onWalletConnectScreen { appName.assertIsDisplayed() } + } + step("Assert approve icon is displayed") { + onWalletConnectScreen { approveIcon.assertIsDisplayed() } + } + step("Assert app URL is displayed") { + onWalletConnectScreen { appUrl.assertIsDisplayed() } + } + } else { + step("Assert wallet name is not displayed") { + onWalletConnectScreen { walletName.assertIsNotDisplayed() } + } + step("Assert app icon is not displayed") { + onWalletConnectScreen { appIcon.assertIsNotDisplayed() } + } + step("Assert app name is not displayed") { + onWalletConnectScreen { appName.assertIsNotDisplayed() } + } + step("Assert approve icon is not displayed") { + onWalletConnectScreen { approveIcon.assertIsNotDisplayed() } + } + step("Assert app URL is not displayed") { + onWalletConnectScreen { appUrl.assertIsNotDisplayed() } + } + step("Assert 'Wallet Connect' image is displayed") { + onWalletConnectScreen { walletConnectImage.assertIsDisplayed() } + } + step("Assert 'No session' title is displayed") { + onWalletConnectScreen { noSessionTitle.assertIsDisplayed() } + } + step("Assert 'No session' text is displayed") { + onWalletConnectScreen { noSessionText.assertIsDisplayed() } + } + } + } fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { + waitForIdle() step("Assert connection details title is displayed") { onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() } } @@ -128,7 +159,7 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() } } step("Assert connected dApp name: '$dAppName'") { - onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) } + onWalletConnectDetailsBottomSheet { appName.assertTextContains(dAppName) } } step("Assert connected network symbol is displayed") { onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt index c111e92406..8490c75e76 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt @@ -9,13 +9,11 @@ import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString -import com.tangem.features.walletconnect.impl.R as WalletConnectImplR class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { val title: KNode = child { - hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect)) hasTestTag(WalletConnectBottomSheetTestTags.TITLE) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt index b9da100325..cb84e644f1 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt @@ -53,6 +53,21 @@ class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvid hasText(getResourceString(R.string.wc_new_connection)) useUnmergedTree = true } + + val walletConnectImage: KNode = child { + hasTestTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE) + useUnmergedTree = true + } + + val noSessionTitle: KNode = child { + hasText(getResourceString(R.string.wc_no_sessions_title)) + useUnmergedTree = true + } + + val noSessionText: KNode = child { + hasText(getResourceString(R.string.wc_no_sessions_desc)) + useUnmergedTree = true + } } internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt new file mode 100644 index 0000000000..d48c6e7e67 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt @@ -0,0 +1,23 @@ +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.BaseButtonTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class WalletConnectScanQrPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val pasteFromClipboardButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.wallet_connect_paste_from_clipboard)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onWalletConnectScanQrScreen(function: WalletConnectScanQrPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt index db5e364f20..13fa9b6a47 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt @@ -2,12 +2,13 @@ package com.tangem.tests import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.TOTAL_BALANCE -import com.tangem.common.extensions.SwipeDirection +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.getWcUri +import com.tangem.common.utils.setClipboardText import com.tangem.scenarios.* import com.tangem.screens.* +import com.tangem.wallet.BuildConfig import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -21,7 +22,7 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink on main screen") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSessionOnMainScreen() { + fun openWalletConnectSessionOnMainScreenTest() { val balance = TOTAL_BALANCE val dAppName = "React App" val deepLinkUri = getWcUri() @@ -37,19 +38,28 @@ class WalletConnectTest : BaseTestCase() { openAppByDeepLink(deepLinkUri) } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } + } + step("Assert 'Connect' button is enabled") { + onWalletConnectBottomSheet { connectButton.assertIsEnabled() } } step("Click on 'Connect' button") { + waitForIdle() onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectScreen(withConnections = true) + } } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } @@ -57,11 +67,11 @@ class WalletConnectTest : BaseTestCase() { step("Check 'Wallet Connect' details bottom sheet") { checkWalletConnectDetailsBottomSheet(dAppName) } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } @@ -70,7 +80,7 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink not on main screen") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSessionNotOnMainScreen() { + fun openWalletConnectSessionNotOnMainScreenTest() { val balance = TOTAL_BALANCE val dAppName = "React App" val deepLinkUri = getWcUri() @@ -82,41 +92,44 @@ class WalletConnectTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses(balance) } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() + checkWalletConnectScreen(false) } step("Create WC session buy deeplink") { openAppByDeepLink(deepLinkUri) } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } } step("Click on 'Connect' button") { + waitForIdle() onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } - } - step("Assert 'Wallet Connect' bottom sheet is displayed") { - onWalletConnectBottomSheet { connectButton.clickWithAssertion() } - } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectScreen(withConnections = true) + } } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } } step("Check 'Wallet Connect' details bottom sheet") { - checkWalletConnectDetailsBottomSheet(dAppName) + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectDetailsBottomSheet(dAppName) + } } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } @@ -125,9 +138,10 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink ") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSession() { + fun openWalletConnectSessionTest() { val balance = TOTAL_BALANCE val dAppName = "React App" + val packageName = BuildConfig.APPLICATION_ID val deepLinkUri = getWcUri() setupHooks().run { @@ -137,32 +151,28 @@ class WalletConnectTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses(balance) } - step("Open recent apps") { - device.uiDevice.pressRecentApps() - } - step("Stop app by swipe") { - swipeVertical(SwipeDirection.UP, startHeightRatio = 0.8f) + step("Kill app") { + device.apps.kill(packageName) } step("Create WC session buy deeplink") { openAppByDeepLink(deepLinkUri) } - step("Open 'Main Screen'") { - openMainScreen() - } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } } step("Click on 'Connect' button") { onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + checkWalletConnectScreen(withConnections = true) } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } @@ -170,11 +180,72 @@ class WalletConnectTest : BaseTestCase() { step("Check 'Wallet Connect' details bottom sheet") { checkWalletConnectDetailsBottomSheet(dAppName) } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) + } + } + } + + @AllureId("887") + @DisplayName("WC: open session by 'Paste from clipboard' button") + @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") + @Test + fun openWalletConnectSessionByClipboardLinkTest() { + val balance = TOTAL_BALANCE + val dAppName = "React App" + val context = device.context + val deepLinkUri = getWcUri() + + setupHooks().run { + step("Set URI to clipboard") { + setClipboardText(context, deepLinkUri) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() + } + step("Click 'New connection' button") { + onWalletConnectScreen { newConnectionButton.performClick() } + } + step("CLick 'Paste from clipboard' button") { + onWalletConnectScanQrScreen { pasteFromClipboardButton.clickWithAssertion() } + } + step("Check 'Wallet Connect' bottom sheet") { + waitForIdle() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } + } + step("Click on 'Connect' button") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.performClick() } + } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } + } + step("Check 'Wallet Connect' screen with connections") { + checkWalletConnectScreen(withConnections = true) + } + step("Click on app icon") { + onWalletConnectScreen { appIcon.performClick() } + } + step("Check 'Wallet Connect' details bottom sheet") { + checkWalletConnectDetailsBottomSheet(dAppName) + } + step("Click on 'Disconnect' button") { + onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } + } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } 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 58e979c7dc..ce88071554 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -67,7 +67,11 @@ internal class DefaultTangemPayStorage @Inject constructor( secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true) } - override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + secureStorage.delete(createOrderIdKey(customerWalletAddress)) + } + + override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { secureStorage.delete(createKey(customerWalletAddress)) secureStorage.delete(createOrderIdKey(customerWalletAddress)) } 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 0f7e759701..d1749cce71 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 @@ -16,7 +16,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase @@ -454,21 +454,17 @@ internal object WalletsDomainModule { @Provides @Singleton - fun provideYieldSupplyApyFlowUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyApyFlowUseCase { + fun provideYieldSupplyApyFlowUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyFlowUseCase { return YieldSupplyApyFlowUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyApyUpdateUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyApyUpdateUseCase { + fun provideYieldSupplyApyUpdateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyUpdateUseCase { return YieldSupplyApyUpdateUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index 70ce92ea14..839504ffbe 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -4,7 +4,7 @@ import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.domain.yield.supply.usecase.* import dagger.Module @@ -82,30 +82,54 @@ internal object YieldSupplyDomainModule { @Provides @Singleton fun provideYieldSupplyGetTokenStatusUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, + yieldSupplyRepository: YieldSupplyRepository, ): YieldSupplyGetTokenStatusUseCase { return YieldSupplyGetTokenStatusUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyGetApyUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyGetApyUseCase { + fun provideYieldSupplyGetApyUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetApyUseCase { return YieldSupplyGetApyUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyGetChartUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyGetChartUseCase { + fun provideYieldSupplyGetChartUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetChartUseCase { return YieldSupplyGetChartUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyIsAvailableUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyIsAvailableUseCase { + return YieldSupplyIsAvailableUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyActivateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyActivateUseCase { + return YieldSupplyActivateUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyDeactivateUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyDeactivateUseCase { + return YieldSupplyDeactivateUseCase( + yieldSupplyRepository = yieldSupplyRepository, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index 3a83c00641..06a188ca28 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -160,4 +160,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Pepecoin, PepecoinTestnet -> null Hyperliquid, HyperliquidTestnet -> null Quai, QuaiTestnet -> null + Linea, LineaTestnet -> null } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 900514a667..e2ce3a2b4a 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,6 +2,7 @@ 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 @@ -19,6 +20,7 @@ import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent 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 @@ -140,9 +142,15 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES } + val mode = when (val portfolio = route.portfolioId) { + is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId) + is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId) + null -> ManageTokensMode.None + } + createComponentChild( context = context, - params = ManageTokensComponent.Params(route.userWalletId, source), + params = ManageTokensComponent.Params(mode, source), componentFactory = manageTokensComponentFactory, ) } @@ -200,7 +208,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = OnrampComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, cryptoCurrency = route.currency, source = route.source, shouldLaunchSepa = route.shouldLaunchSepa, @@ -274,7 +282,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = TokenDetailsComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param + userWalletId = route.userWalletId, currency = route.currency, ), componentFactory = tokenDetailsComponentFactory, @@ -284,7 +292,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = StakingComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, cryptoCurrencyId = route.cryptoCurrencyId, yieldId = route.yieldId, ), @@ -297,7 +305,7 @@ internal class ChildFactory @Inject constructor( params = SwapComponent.Params( currencyFrom = route.currencyFrom, currencyTo = route.currencyTo, - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, isInitialReverseOrder = route.isInitialReverseOrder, screenSource = route.screenSource, ), @@ -308,7 +316,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = SendComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, currency = route.currency, transactionId = route.transactionId, amount = route.amount, 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 d4566f6a58..ce692050bc 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 @@ -11,7 +11,6 @@ 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.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse @@ -51,50 +50,25 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class CurrencyDetails( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, - ) : AppRoute(path = "/currency_details/${portfolioId.stringValue}/${currency.id.value}") { - companion object { - operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) = CurrencyDetails( - portfolioId = PortfolioId(userWalletId), - currency = currency, - ) - } - } + ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") @Serializable data class Send( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, val transactionId: String? = null, val amount: String? = null, val tag: String? = null, val destinationAddress: String? = null, ) : AppRoute( - path = "/send/${portfolioId.stringValue}/${currency.id.value}?" + + path = "/send/${userWalletId.stringValue}/${currency.id.value}?" + "&$transactionId" + "&$amount" + "&$tag" + "&$destinationAddress", - ) { - companion object { - operator fun invoke( - userWalletId: UserWalletId, - currency: CryptoCurrency, - transactionId: String? = null, - amount: String? = null, - tag: String? = null, - destinationAddress: String? = null, - ) = Send( - portfolioId = PortfolioId(userWalletId), - currency = currency, - transactionId = transactionId, - amount = amount, - tag = tag, - destinationAddress = destinationAddress, - ) - } - } + ) @Serializable data class Details( @@ -147,8 +121,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( val source: Source, - val userWalletId: UserWalletId? = null, - ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId") { + val portfolioId: PortfolioId? = null, + ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { enum class Source { STORIES, @@ -199,51 +173,26 @@ sealed class AppRoute(val path: String) : Route { data class Swap( val currencyFrom: CryptoCurrency, val currencyTo: CryptoCurrency? = null, - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val isInitialReverseOrder: Boolean = false, val screenSource: String, ) : AppRoute( path = "/swap" + "/${currencyFrom.id.value}" + "/${currencyTo?.id?.value}" + - "/${portfolioId.stringValue}" + + "/${userWalletId.stringValue}" + "/$isInitialReverseOrder", - ) { - companion object { - operator fun invoke( - userWalletId: UserWalletId, - currencyFrom: CryptoCurrency, - currencyTo: CryptoCurrency? = null, - isInitialReverseOrder: Boolean = false, - screenSource: String, - ) = Swap( - portfolioId = PortfolioId(userWalletId), - currencyFrom = currencyFrom, - currencyTo = currencyTo, - isInitialReverseOrder = isInitialReverseOrder, - screenSource = screenSource, - ) - } - } + ) @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") @Serializable data class Staking( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, val yieldId: String, - ) : AppRoute(path = "/staking/${portfolioId.stringValue}/${cryptoCurrencyId.value}/$yieldId") { - companion object { - operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yieldId: String) = - Staking( - portfolioId = PortfolioId(userWalletId), - cryptoCurrencyId = cryptoCurrencyId, - yieldId = yieldId, - ) - } - } + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId") @Serializable data class PushNotification( @@ -287,25 +236,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Onramp( val source: OnrampSource, - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, val shouldLaunchSepa: Boolean = false, - ) : AppRoute(path = "/onramp/${portfolioId.stringValue}/${currency.symbol}"), RouteBundleParams { + ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - operator fun invoke( - source: OnrampSource, - userWalletId: UserWalletId, - currency: CryptoCurrency, - launchSepa: Boolean = false, - ) = Onramp( - source = source, - portfolioId = PortfolioId(userWalletId), - currency = currency, - shouldLaunchSepa = launchSepa, - ) - } } @Serializable diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt new file mode 100644 index 0000000000..51f13b848f --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt @@ -0,0 +1,53 @@ +package com.tangem.common.ui.account + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.text.TextStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Displays account name with icon + * + * @param name account name + * @param icon portfolio account icon model + * @param iconSize portfolio account icon size + * @param nameStyle account name style + * @param nameColor account name color + * @see AccountIcon + */ +@Composable +fun AccountLabel( + name: TextReference, + icon: CryptoPortfolioIconUM, + iconSize: AccountIconSize, + modifier: Modifier = Modifier, + nameStyle: TextStyle = TangemTheme.typography.subtitle2, + nameColor: Color = TangemTheme.colors.text.tertiary, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + AccountIcon( + name = name, + icon = icon, + size = iconSize, + ) + Text( + text = name.resolveReference(), + style = nameStyle, + color = nameColor, + maxLines = 1, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index fdd122631d..f0fa604881 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -13,21 +13,17 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData -import com.tangem.common.ui.amountScreen.ui.amountField import com.tangem.common.ui.amountScreen.ui.amountFieldV2 -import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.core.ui.res.TangemThemePreview /** * Amount screen with field * @param amountState amount state - * @param isBalanceHidden flag hidden balances * @param clickIntents amount screen clicks */ @Composable fun AmountScreenContent( amountState: AmountState, - isBalanceHidden: Boolean, clickIntents: AmountScreenClickIntents, modifier: Modifier = Modifier, extraContent: (@Composable () -> Unit)? = null, @@ -38,32 +34,17 @@ fun AmountScreenContent( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - if (amountState.isRedesignEnabled) { - amountFieldV2( - amountState = amountState, - onValueChange = clickIntents::onAmountValueChange, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - onCurrencyChange = clickIntents::onCurrencyChangeClick, - onMaxAmountClick = clickIntents::onMaxValueClick, - ) - if (extraContent != null) { - item("EXTRA_CONTENT_KEY") { - extraContent() - } + amountFieldV2( + amountState = amountState, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + onCurrencyChange = clickIntents::onCurrencyChangeClick, + onMaxAmountClick = clickIntents::onMaxValueClick, + ) + if (extraContent != null) { + item("EXTRA_CONTENT_KEY") { + extraContent() } - } else if (amountState is AmountState.Data) { - amountField( - amountState = amountState, - isBalanceHidden = isBalanceHidden, - onValueChange = clickIntents::onAmountValueChange, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - buttons( - segmentedButtonConfig = amountState.segmentedButtonConfig, - clickIntents = clickIntents, - isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, - selectedButton = amountState.selectedButton, - ) } } } @@ -78,7 +59,6 @@ private fun SendAmountContentPreview( TangemThemePreview { AmountScreenContent( amountState = amountState, - isBalanceHidden = false, clickIntents = AmountScreenClickIntentsStub, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt index 7406013c29..0faf2844b8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -38,7 +38,6 @@ class AmountCurrencyTransformer( keyboardType = KeyboardType.Number, ), ), - selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index f96fa69bd0..783682376d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -70,10 +70,9 @@ class AmountReduceByTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index 0afd5d8bc5..864db79015 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -64,10 +64,9 @@ class AmountReduceToTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 36f37a67ab..cdcd0771cc 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -1,104 +1,35 @@ package com.tangem.common.ui.amountScreen.converters -import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter -import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2 import com.tangem.common.ui.amountScreen.models.AmountParameters -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import kotlinx.collections.immutable.persistentListOf - -/** - * Converts initial [String] to [AmountState] - * - * @property clickIntents amount screen clicks - * @property appCurrencyProvider selected app currency provider - * @property maxEnterAmount max enter amount data - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider - * @property iconStateConverter currency icon converter - */ -@Deprecated("Use AmountStateConverterV2") -class AmountStateConverter( - private val clickIntents: AmountScreenClickIntents, - private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, - private val maxEnterAmount: EnterAmountBoundary, - private val iconStateConverter: CryptoCurrencyToIconStateConverter, -) : Converter { - - private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountFieldConverter( - clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - ) - } - - override fun convert(value: AmountParameters): AmountState { - val appCurrency = appCurrencyProvider() - val status = cryptoCurrencyStatusProvider() - val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } - val crypto = maxEnterAmount.amount.format { crypto(status.currency) } - val hasNoFeeRate = status.value.fiatRate.isNullOrZero() - - return AmountState.Data( - title = value.title, - availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), - availableBalanceCrypto = stringReference(crypto), - availableBalanceFiat = stringReference(fiat), - tokenName = stringReference(status.currency.name), - tokenIconState = iconStateConverter.convert(status), - amountTextField = amountFieldConverter.convert(value.value), - isPrimaryButtonEnabled = false, - appCurrency = appCurrency, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference(status.currency.symbol), - iconState = iconStateConverter.convertCustom( - value = status, - forceGrayscale = hasNoFeeRate, - showCustomTokenBadge = false, - ), - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), - isSegmentedButtonsEnabled = !hasNoFeeRate, - selectedButton = 0, - isRedesignEnabled = false, - ) - } -} /** * Converts initial [String] to [AmountState] * * @property clickIntents amount screen clicks * @property appCurrency selected app currency - * @property maxEnterAmount max enter amount data * @property cryptoCurrencyStatus current cryptocurrency status + * @property maxEnterAmount max enter amount data * @property iconStateConverter currency icon converter * @property isBalanceHidden is balance hidden status */ @Suppress("LongParameterList") -class AmountStateConverterV2( +class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrency: AppCurrency, private val cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -108,7 +39,7 @@ class AmountStateConverterV2( ) : Converter { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountFieldConverterV2( + AmountFieldConverter( clickIntents = clickIntents, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, @@ -118,19 +49,13 @@ class AmountStateConverterV2( override fun convert(value: AmountParameters): AmountState { val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } - val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) { - return AmountState.Empty(isRedesignEnabled = true) + return AmountState.Empty } return AmountState.Data( title = value.title, - availableBalance = combinedReference( - stringReference(crypto), - stringReference(" $DOT "), - stringReference(fiat), - ).orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceFiat = if (isBalanceHidden) { TextReference.EMPTY @@ -145,25 +70,6 @@ class AmountStateConverterV2( amountTextField = amountFieldConverter.convert(value.value), isPrimaryButtonEnabled = false, appCurrency = appCurrency, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference(cryptoCurrencyStatus.currency.symbol), - iconState = iconStateConverter.convertCustom( - value = cryptoCurrencyStatus, - forceGrayscale = noFeeRate, - showCustomTokenBadge = false, - ), - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), - isSegmentedButtonsEnabled = !noFeeRate, - selectedButton = 0, - isRedesignEnabled = true, ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 53bdef9c57..21a6f4e7f5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -2,7 +2,10 @@ package com.tangem.common.ui.amountScreen.converters.field import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -32,14 +35,7 @@ class AmountBoundaryUpdateTransformer( val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } - val availableBalance = combinedReference( - stringReference(crypto), - stringReference(" $DOT "), - stringReference(fiat), - ) - return prevState.copy( - availableBalance = availableBalance.orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceFiat = if (isBalanceHidden) { TextReference.EMPTY diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index f0baf3a1e9..411de67feb 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -13,75 +13,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import java.math.BigDecimal -/** - * Converts initial [String] to [AmountFieldModel] - * - * @property clickIntents amount screen clicks - * @property appCurrencyProvider selected app currency provider - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider - */ -@Deprecated("Use AmountFieldConverterV2") -class AmountFieldConverter( - private val clickIntents: AmountScreenClickIntents, - private val cryptoCurrencyStatusProvider: Provider, - private val appCurrencyProvider: Provider, -) : Converter { - - override fun convert(value: String): AmountFieldModel { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO - val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) - val fiatRate = cryptoCurrencyStatus.value.fiatRate - val (fiatValue, fiatDecimal) = when { - fiatRate.isNullOrZero() -> "" to null - value.isEmpty() -> "" to BigDecimal.ZERO - else -> { - val fiatDecimal = fiatRate?.multiply(cryptoDecimal) - val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() - fiatValue to fiatDecimal - } - } - val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() - return AmountFieldModel( - value = value, - fiatValue = fiatValue, - onValueChange = clickIntents::onAmountValueChange, - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions( - onDone = { clickIntents.onAmountNext() }, - ), - isFiatValue = false, - cryptoAmount = cryptoAmount, - fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), - isError = false, - isWarning = false, - error = TextReference.EMPTY, - isFiatUnavailable = fiatRate == null, - isValuePasted = false, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - } - - private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( - currencySymbol = appCurrency.symbol, - value = fiatValue, - decimals = FIAT_DECIMALS, - type = AmountType.FiatType(appCurrency.code), - ) - - private companion object { - private const val FIAT_DECIMALS = 2 - } -} - /** * Converts initial [String] to [AmountFieldModel] * @@ -89,7 +24,7 @@ class AmountFieldConverter( * @property appCurrency selected app currency * @property cryptoCurrencyStatus current cryptocurrency status */ -class AmountFieldConverterV2( +class AmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index a8c20c0745..db245b6b30 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -54,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer( isError = isLessThanMinimumIfProvided, error = when { isLessThanMinimumIfProvided -> { - val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) } + val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt deleted file mode 100644 index 23469c4c03..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.common.ui.amountScreen.models - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.TextReference - -/** - * Segmented buttons config - * - * @param title button title - * @param iconState currency icon state - * @param iconUrl currency icon url - * @param isFiat is fiat currency - */ -@Immutable -data class AmountSegmentedButtonsConfig( - val title: TextReference, - val iconState: CurrencyIconState? = null, - val iconUrl: String? = null, - val isFiat: Boolean, -) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index a063b984f5..d207f14e5a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import kotlinx.collections.immutable.PersistentList import java.math.BigDecimal /** Model for amount state */ @@ -12,18 +11,13 @@ import java.math.BigDecimal sealed class AmountState { abstract val isPrimaryButtonEnabled: Boolean - abstract val isRedesignEnabled: Boolean /** * @param isPrimaryButtonEnabled indicates if next state button enabled * @param title title - * @param availableBalance user crypto currency balance with fiat balance * @param availableBalanceCrypto user crypto currency balance in crypto * @param availableBalanceFiat user crypto currency balance in fiat * @param tokenIconState crypto currency icon state - * @param segmentedButtonConfig currency switcher config - * @param selectedButton selected currency index - * @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param amountTextField amount field state * @param appCurrency app currency * @param isEditingDisabled indicated whether amount is editable @@ -32,17 +26,11 @@ sealed class AmountState { */ data class Data( override val isPrimaryButtonEnabled: Boolean, - override val isRedesignEnabled: Boolean, val title: TextReference, - @Deprecated("Remove with SEND_REDESIGNED toggle") - val availableBalance: TextReference, val availableBalanceCrypto: TextReference, val availableBalanceFiat: TextReference, val tokenName: TextReference, val tokenIconState: CurrencyIconState, - val segmentedButtonConfig: PersistentList, - val selectedButton: Int, - val isSegmentedButtonsEnabled: Boolean, val amountTextField: AmountFieldModel, val appCurrency: AppCurrency, val isEditingDisabled: Boolean = false, @@ -50,8 +38,7 @@ sealed class AmountState { val isIgnoreReduce: Boolean = false, ) : AmountState() - data class Empty( - override val isPrimaryButtonEnabled: Boolean = false, - override val isRedesignEnabled: Boolean, - ) : AmountState() + data object Empty : AmountState() { + override val isPrimaryButtonEnabled: Boolean = false + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 98f7c1e21d..0624a2c89d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -3,7 +3,6 @@ package com.tangem.common.ui.amountScreen.preview import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference @@ -12,31 +11,18 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import com.tangem.utils.StringsSigns -import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal object AmountStatePreviewData { - val emptyState = AmountState.Empty(isRedesignEnabled = true) + val emptyState = AmountState.Empty val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), - availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceFiat = stringReference("1 232 129,12 \$"), tokenIconState = CurrencyIconState.Loading, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference("USDT"), - iconState = CurrencyIconState.Locked, - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference("USD"), - isFiat = true, - ), - ), appCurrency = AppCurrency.Default, tokenName = stringReference("Tether"), amountTextField = AmountFieldModel( @@ -65,12 +51,9 @@ object AmountStatePreviewData { isValuePasted = false, onValuePastedTriggerDismiss = {}, ), - isSegmentedButtonsEnabled = true, - selectedButton = 0, - isRedesignEnabled = false, ) - val amountWithValueState = amountState.copy( + private val amountWithValueState = amountState.copy( amountTextField = amountState.amountTextField.copy( value = "100.00", cryptoAmount = amountState.amountTextField.cryptoAmount.copy( @@ -84,16 +67,10 @@ object AmountStatePreviewData { ) val amountStateV2 = amountState.copy( - isRedesignEnabled = true, - availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"), ) - val amountWithValueFiatState = amountWithValueState.copy( - amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), - ) - val amountStateV2WithoutRates = amountState.copy( amountTextField = amountState.amountTextField.copy( fiatAmount = amountState.amountTextField.fiatAmount.copy( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index 62fe6dc051..9ec184b76e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -16,10 +16,13 @@ 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.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -59,7 +62,16 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing16), ) { - CurrencyIcon(state = amountState.tokenIconState) + Text( + text = amountState.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH(20.dp) + CurrencyIcon( + state = amountState.tokenIconState, + iconSize = 40.dp, + ) ResizableText( text = firstAmount, style = TangemTheme.typography.h2, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt deleted file mode 100644 index 64d1c8f25c..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.common.ui.amountScreen.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListScope -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.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.testTag -import com.tangem.common.ui.R -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.icon.CurrencyIcon -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.SendScreenTestTags -import kotlinx.collections.immutable.PersistentList - -private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" - -internal fun LazyListScope.buttons( - segmentedButtonConfig: PersistentList, - clickIntents: AmountScreenClickIntents, - isSegmentedButtonsEnabled: Boolean, - selectedButton: Int, -) { - item( - key = AMOUNT_BUTTONS_KEY, - ) { - val hapticFeedback = LocalHapticFeedback.current - Row { - if (segmentedButtonConfig.isNotEmpty()) { - SegmentedButtons( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size40), - config = segmentedButtonConfig, - showIndication = false, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onCurrencyChangeClick(it.isFiat) - }, - initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), - isEnabled = isSegmentedButtonsEnabled, - ) { - AmountCurrencyButton( - button = it, - isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, - ) - } - } else { - SpacerWMax() - } - Text( - text = stringResourceSafe(R.string.send_max_amount), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .height(TangemTheme.dimens.size40) - .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26)) - .background(TangemTheme.colors.button.secondary) - .clickable { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onMaxValueClick() - } - .padding( - vertical = TangemTheme.dimens.spacing10, - horizontal = TangemTheme.dimens.spacing34, - ) - .testTag(SendScreenTestTags.MAX_BUTTON), - ) - } - } -} - -@Composable -private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { - Row( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing10, - ) - .testTag(SendScreenTestTags.CURRENCY_BUTTON), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - val iconModifier = Modifier - .size(TangemTheme.dimens.size18) - .padding(horizontal = TangemTheme.dimens.spacing1) - if (button.isFiat) { - FiatIcon( - url = button.iconUrl, - size = TangemTheme.dimens.size18, - isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier.testTag(SendScreenTestTags.FIAT_ICON), - ) - } else if (button.iconState != null) { - CurrencyIcon( - state = button.iconState, - shouldDisplayNetwork = false, - modifier = iconModifier.testTag(SendScreenTestTags.CURRENCY_ICON), - ) - } - Text( - text = button.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - ), - ) - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt deleted file mode 100644 index 848bd0ab36..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.tangem.common.ui.amountScreen.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeightIn -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.Companion.BottomCenter -import androidx.compose.ui.Alignment.Companion.TopCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -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.res.TangemTheme -import com.tangem.core.ui.test.SendScreenTestTags -import com.tangem.core.ui.utils.rememberDecimalFormat -import kotlinx.coroutines.delay - -@Composable -internal fun AmountField( - amountField: AmountFieldModel, - appCurrencyCode: String, - onValueChange: (String) -> Unit, - onValuePastedTriggerDismiss: () -> Unit, -) { - val decimalFormat = rememberDecimalFormat() - val isFiatValue = amountField.isFiatValue - val currencyCode = if (isFiatValue) appCurrencyCode else null - val (primaryAmount, primaryValue) = if (isFiatValue) { - amountField.fiatAmount to amountField.fiatValue - } else { - amountField.cryptoAmount to amountField.value - } - val requester = remember { FocusRequester() } - val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1 - AmountTextField( - value = primaryValue, - decimals = primaryAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = primaryAmount.decimals, - symbol = primaryAmount.currencySymbol, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - symbolColor = symbolColor, - ), - onValueChange = onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing24, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32), - ) - - LaunchedEffect(key1 = Unit) { - delay(timeMillis = 200) - requester.requestFocus() - } - - AmountSecondary(amountField, appCurrencyCode) -} - -@Composable -private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { - val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount - Box( - modifier = Modifier - .fillMaxWidth() - .animateContentSize() - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - ) { - val text = if (amountField.isFiatValue) { - secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } - } else { - secondaryAmount.value.format { - fiat( - fiatCurrencySymbol = secondaryAmount.currencySymbol, - fiatCurrencyCode = appCurrencyCode, - ) - } - } - Text( - text = text, - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .align(TopCenter) - .padding(bottom = TangemTheme.dimens.spacing32) - .testTag(SendScreenTestTags.SECONDARY_AMOUNT), - ) - AmountFieldError( - isError = amountField.isError, - isWarning = amountField.isWarning, - error = amountField.error, - modifier = Modifier - .align(BottomCenter) - .padding( - top = TangemTheme.dimens.spacing20, - bottom = TangemTheme.dimens.spacing12, - ), - ) - } -} - -@Composable -private fun AmountFieldError( - isError: Boolean, - isWarning: Boolean, - error: TextReference, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = isError || isWarning, - enter = fadeIn(), - exit = fadeOut(), - modifier = modifier, - ) { - val errorText = remember(this, error) { error } - val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention - Text( - text = errorText.resolveReference(), - style = TangemTheme.typography.caption2, - color = color, - textAlign = TextAlign.Center, - ) - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 6fd563f173..d5973e72b8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -15,8 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState @@ -25,68 +23,12 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.orMaskWithStars 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.SendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" -internal fun LazyListScope.amountField( - amountState: AmountState.Data, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, - onValueChange: (String) -> Unit, - onValuePastedTriggerDismiss: () -> Unit, -) { - item(key = AMOUNT_FIELD_KEY) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = amountState.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14) - .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE), - ) - - val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", - ) { - Text( - text = it, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2) - .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT), - ) - } - CurrencyIcon( - state = amountState.tokenIconState, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32), - ) - AmountField( - amountField = amountState.amountTextField, - appCurrencyCode = amountState.appCurrency.code, - onValueChange = onValueChange, - onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, - ) - } - } -} - internal fun LazyListScope.amountFieldV2( amountState: AmountState, modifier: Modifier = Modifier, @@ -183,46 +125,49 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi @Composable private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) { AnimatedContent( - targetState = amountUM !is AmountState.Data, + targetState = amountUM, modifier = modifier, - ) { isContent -> - if (isContent) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.width(56.dp), - ) - TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), - ) - } - } else { - val amountUM = amountUM as AmountState.Data - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = amountUM.tokenName.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - ) - Row { - EllipsisText( - text = amountUM.availableBalanceCrypto.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length), - modifier = Modifier.weight(1f, fill = false), + ) { currentAmount -> + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + when (currentAmount) { + is AmountState.Data -> { + val amountUM = amountUM as AmountState.Data + Text( + text = amountUM.tokenName.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, ) - EllipsisText( - text = amountUM.availableBalanceFiat.resolveReference(), + Row { + EllipsisText( + text = amountUM.availableBalanceCrypto.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd( + amountUM.amountTextField.cryptoAmount.currencySymbol.length, + ), + modifier = Modifier.weight(1f, fill = false), + ) + EllipsisText( + text = amountUM.availableBalanceFiat.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd( + amountUM.amountTextField.fiatAmount.currencySymbol.length, + ), + ) + } + } + AmountState.Empty -> { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + TextShimmer( style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.fiatAmount.currencySymbol.length), + modifier = Modifier.width(72.dp), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index a52403de79..47233d7291 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -55,7 +55,6 @@ fun NavigationButtonsBlock( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - PreviousButton(state?.prevButton) NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index 59bab529ed..6191b4de9c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -8,7 +8,6 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton?, - val prevButton: NavigationButton?, val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 76ebce30a8..6c5a8d5991 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -3,7 +3,6 @@ package com.tangem.common.ui.navigationButtons.preview import com.tangem.common.ui.R import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference internal object NavigationButtonsPreview { @@ -26,16 +25,6 @@ internal object NavigationButtonsPreview { onClick = {}, ) - private val prev = NavigationButton( - textReference = TextReference.EMPTY, - iconRes = R.drawable.ic_back_24, - isSecondary = true, - isIconVisible = true, - shouldShowProgress = false, - isEnabled = true, - onClick = {}, - ) - private val finished = NavigationButton( textReference = resourceReference(R.string.common_close), isSecondary = false, @@ -47,7 +36,6 @@ internal object NavigationButtonsPreview { val allButtons = NavigationButtonsState.Data( primaryButton = finished, - prevButton = prev, extraButtons = extraButtons, txUrl = "https://tangem.com", onTextClick = {}, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt index 20aae40e90..2158ef7ede 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt @@ -15,7 +15,7 @@ import retrofit2.http.Query interface YieldSupplyApi { @GET("api/v1/yield/markets") - suspend fun getYieldMarkets(@Query("chainId") chainId: Int? = null): ApiResponse + suspend fun getYieldMarkets(@Query("chainId") chainId: String? = null): ApiResponse @GET("api/v1/yield/token/{chainId}/{tokenAddress}") suspend fun getYieldTokenStatus( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt index 3f36276519..4157c42c41 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.utils.SerializeNulls @JsonClass(generateAdapter = true) data class SaveWalletAccountsResponse( - @Json(name = "accounts") val accounts: List, -) \ No newline at end of file + @Json(name = "accounts") val accounts: List, +) { + + @SerializeNulls + @JsonClass(generateAdapter = true) + data class AccountDTO( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String?, + @Json(name = "derivation") val derivationIndex: Int, + @Json(name = "icon") val icon: String, + @Json(name = "iconColor") val iconColor: String, + ) + + companion object { + + operator fun invoke(accounts: List): SaveWalletAccountsResponse { + return SaveWalletAccountsResponse( + accounts = accounts.map { accountDto -> + AccountDTO( + id = accountDto.id, + name = accountDto.name, + derivationIndex = accountDto.derivationIndex, + icon = accountDto.icon, + iconColor = accountDto.iconColor, + ) + }, + ) + } + } +} \ No newline at end of file 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 37ea5507c5..1da93e6b44 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.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -28,6 +29,7 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() + .add(SerializeNullsFactory) .add( PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type") .withSubtype(ProviderModel.Public::class.java, "public") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt index 8b174ebbc1..2b7cfecd0d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt @@ -4,11 +4,11 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes -import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -34,7 +34,7 @@ object YieldSupplyModule { persistenceStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( moshi = moshi, - types = listTypes(), + types = listTypes(), defaultValue = emptyList(), ), produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") }, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index 1ed58f2220..bf8bf7fa78 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -12,5 +12,7 @@ interface TangemPayStorage { suspend fun getOrderId(customerWalletAddress: String): String? - suspend fun clear(customerWalletAddress: String) + suspend fun clearOrderId(customerWalletAddress: String) + + suspend fun clearAll(customerWalletAddress: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt index b282890523..7f4dbc5eb7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt @@ -1,21 +1,21 @@ package com.tangem.datasource.local.yieldsupply import androidx.datastore.core.DataStore -import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull internal class DefaultYieldMarketsStore( - private val persistenceStore: DataStore>, + private val persistenceStore: DataStore>, ) : YieldMarketsStore { - override fun get(): Flow> = persistenceStore.data + override fun get(): Flow> = persistenceStore.data - override suspend fun getSyncOrNull(): List? { + override suspend fun getSyncOrNull(): List? { return persistenceStore.data.firstOrNull() } - override suspend fun store(items: List) { + override suspend fun store(items: List) { persistenceStore.updateData { _ -> items } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt index c78c131d7a..8857e6f40a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.local.yieldsupply -import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import kotlinx.coroutines.flow.Flow interface YieldMarketsStore { - fun get(): Flow> + fun get(): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(): List? - suspend fun store(items: List) + suspend fun store(items: List) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt new file mode 100644 index 0000000000..8361150150 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.utils + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class SerializeNulls \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt new file mode 100644 index 0000000000..ac77f01f7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.utils + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +/** + * Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls]. + * +[REDACTED_AUTHOR] + */ +internal object SerializeNullsFactory : JsonAdapter.Factory { + + override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { + val rawType = Types.getRawType(type) + if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) { + return null + } + + val nextAdapter: JsonAdapter = moshi.nextAdapter(this, type, annotations) + + return nextAdapter.serializeNulls() + } +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt new file mode 100644 index 0000000000..9dafd03360 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt @@ -0,0 +1,51 @@ +package com.tangem.datasource.utils + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +// --- DTO --- +@SerializeNulls +@JsonClass(generateAdapter = true) +data class UserWithNulls(val id: String?, val name: String?) + +@JsonClass(generateAdapter = true) +data class UserWithoutNulls(val id: String?, val name: String?) + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SerializeNullsFactoryTest { + + private val moshi = Moshi.Builder() + .add(SerializeNullsFactory) + .build() + + @Test + fun `should serialize nulls for annotated class`() { + val adapter = moshi.adapter(UserWithNulls::class.java) + + val json = adapter.toJson(UserWithNulls(id = null, name = "John")) + + assertThat(json).isEqualTo("""{"id":null,"name":"John"}""") + } + + @Test + fun `should skip nulls for non-annotated class`() { + val adapter = moshi.adapter(UserWithoutNulls::class.java) + + val json = adapter.toJson(UserWithoutNulls(id = null, name = "John")) + + assertThat(json).isEqualTo("""{"name":"John"}""") + } + + @Test + fun `should deserialize annotated class correctly`() { + val adapter = moshi.adapter(UserWithNulls::class.java) + + val json = """{"id":null,"name":"Jane"}""" + val result = adapter.fromJson(json) + + assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane")) + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 4a69572db8..97d1117305 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1688,6 +1688,7 @@ 承認を確定する 資産残高に関するテキスト [プレースホルダー] あなたの%sはAaveに預けられています + チャートを読み込めません・・ 受け取った金額%1$s %2$sはAaveに入金されませんでした。 %1$s%% を獲得 利用可能 @@ -1705,6 +1706,7 @@ 最大手数料 手数料ポリシー ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。 + 過去のリターン ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー] トークン承認が必要 ネットワーク接続を確認してください diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6d0accb1ee..cf16e56da7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -80,7 +80,7 @@ Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. Ошибка активации - Ваш промокод был успешно активирован. Награда будет зачислена на ваш счёт в течение 14 дней. + Ваш промокод успешно активирован. Бонус 10 USDT в Bitcoin будет зачислен через 14 дней. Промокод активирован Этот промокод уже был использован и не может быть активирован повторно. Код недоступен @@ -132,7 +132,7 @@ Аналитика Применить Одобрение - Разрешить + Подтвердить Внимание Доступные сети Баланс: %s @@ -1202,7 +1202,7 @@ В сумму включена комиссия провайдера сервиса. \n\nПроскальзывание провайдера составляет до %s Информация Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. - Разрешение + Подтвердить Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку. Вы отправляете Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. @@ -1559,7 +1559,6 @@ Нет, отправить все Уменьшить на %s XTZ Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ - Выдать разрешение Невозможно загрузить график Полученная сумма, %1$s %2$s, не была зачислена на Aave. Сетевая комиссия сейчас слишком высокая. Ожидаем, пока она упадёт ниже вашего лимита. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 50216b2d4c..6479a28a3f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -135,7 +135,7 @@ You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. An error occurred while processing your promo code. Please try again later. Activation error - Your promo code was successfully activated. A reward will be credited to your account within 14 days. + Your promo code was successfully activated. A bonus of 10 USDT in Bitcoin will be credited to your account within 14 days. Promo Code Activated This promo code has already been used and cannot be activated again. Code unavailable @@ -1117,8 +1117,8 @@ This will remove the wallet from the application. The wallet itself can be added again. Name Put your token to work - A network fee is a small payment required to process and confirm your transaction on the blockchain. - To start staking, your TON account must be activated with a self-transaction of 1 TON. The funds stay in your wallet — this step only enables your account for staking. + A network fee is a small payment to process and confirm your transaction on the blockchain. + To start staking, your TON account must be activated with a self-transaction of 1 TON. The funds stay in your wallet — this step only enables your account for staking. Account activation The amount to stake must be at least %s Staking amount will be rounded to %1$s TRX due to network rules. @@ -1731,20 +1731,13 @@ URI already used WalletConnect Suspicious transaction - Already have Tangem? - Thousands of assets Best in class hardware wallet Fast delivery - Start in one tap - Seamless and secure Simple to use Create a hardware wallet with Tangem. Slim as a bank card, secure as a bank vault. Create or import a software wallet - Create or import a software wallet on your phone Start with Mobile Wallet Other method - Use Tangem Hardware Wallet - Learn more & buy Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index ff3b869c1f..fa775ca36c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -60,10 +60,10 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M @Composable fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { if (state.isExpanded) { - ExpandedPortfolioHeader(state.state, modifier) + ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier) } else { TokenItem( - state = state.state, + state = state.tokenItemUM, isBalanceHidden = isBalanceHidden, modifier = modifier, ) @@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } @Composable -private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = Modifier) { +private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -130,11 +130,13 @@ private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = ) } - Icon( - modifier = Modifier.size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_minimize_24), - tint = TangemTheme.colors.icon.inactive, - contentDescription = null, - ) + if (isCollapsable) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_minimize_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } } } \ No newline at end of file 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 24da6e9d28..7797159dc1 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable 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 /** Tokens list item state */ @Immutable @@ -41,11 +42,12 @@ sealed interface TokensListItemUM { } data class Portfolio( - val state: TokenItemState, + val tokenItemUM: TokenItemState, val isExpanded: Boolean, - val tokens: List, + val isCollapsable: Boolean, + val tokens: ImmutableList, ) : TokensListItemUM { - override val id: String = state.id + override val id: String = tokenItemUM.id } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt new file mode 100644 index 0000000000..75f52c5be4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.decompose + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +@Stable +interface ComposableListContentComponent { + + val uiState: StateFlow + + fun LazyListScope.content(uiState: T, modifier: Modifier) + + companion object { + val EMPTY = EmptyComposableListContentComponent + } +} + +object EmptyComposableListContentComponent : ComposableListContentComponent { + override val uiState: StateFlow = MutableStateFlow(Unit) + + override fun LazyListScope.content(uiState: Unit, modifier: Modifier) { /* no-op */ + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index e106f177ab..d0a89131e0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -96,6 +96,7 @@ fun getActiveIconRes(blockchainId: String): Int { "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "quai", "quai/test" -> R.drawable.img_quai_22 + "linea", "linea/test" -> R.drawable.img_linea_22 else -> R.drawable.ic_alert_24 } } @@ -190,6 +191,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "quai", "quai/test" -> R.drawable.img_quai_22 + "linea", "linea/test" -> R.drawable.img_linea_22 else -> R.drawable.ic_alert_24 } } @@ -287,6 +289,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22 "quai", "quai/test" -> R.drawable.ic_quai_22 + "linea", "linea/test" -> R.drawable.ic_linea_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt index 8280fc5021..53400f7588 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt @@ -7,4 +7,5 @@ object WalletConnectScreenTestTags { const val APP_NAME = "WALLET_CONNECT_SCREEN_APP_NAME" const val APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON" const val APP_URL = "WALLET_CONNECT_SCREEN_APP_URL" + const val WALLET_CONNECT_IMAGE = "WALLET_CONNECT_SCREEN_WALLET_CONNECT_IMAGE" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_linea_22.xml b/core/ui/src/main/res/drawable/ic_linea_22.xml new file mode 100644 index 0000000000..32b14b57c5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_linea_22.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_linea_22.xml b/core/ui/src/main/res/drawable/img_linea_22.xml new file mode 100644 index 0000000000..5c2542faaa --- /dev/null +++ b/core/ui/src/main/res/drawable/img_linea_22.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index d5c297866a..58b5628a0d 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { // region Project - Domain api(projects.domain.account) api(projects.domain.card) + api(projects.domain.common) api(projects.domain.models) // endregion diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt index d4c993f59f..b2744a7452 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -28,7 +28,7 @@ internal class AccountListConverter @AssistedInject constructor( override fun convert(value: GetWalletAccountsResponse): AccountList { return AccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(), totalAccounts = value.wallet.totalAccounts, sortType = TokensSortTypeConverter.convert(value.wallet.sort), diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt index 07c76c7c24..8f26a20de5 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt @@ -1,7 +1,6 @@ package com.tangem.data.account.converter import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse -import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.account.Account import com.tangem.utils.converter.Converter @@ -21,8 +20,8 @@ internal object SaveWalletAccountsResponseConverter : Converter) -> Unit, storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit, - ) { + ): GetWalletAccountsResponse? { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) if (isResponseUpToDate) { Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") - return + return savedAccountsResponse } - val (accountDTOs, userTokensResponse) = if (savedAccountsResponse == null) { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val response = savedAccountsResponse ?: defaultWalletAccountsResponseFactory.create( + userWalletId = userWalletId, + userTokensResponse = getFromLegacyStore(userWalletId), + ) - createDefaultAccountDTOs(userWallet) to getFromLegacyStore(userWalletId).orDefault(userWallet) - } else { - savedAccountsResponse.accounts to savedAccountsResponse.toUserTokensResponse() - } + val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse() val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND) if (isNotFoundError) { @@ -82,49 +69,18 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse) } - val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse) storeWalletAccounts(userWalletId, response) - } - private fun createDefaultAccountDTOs(userWallet: UserWallet): List { - val accounts = AccountList.empty(userWallet).accounts - .filterIsInstance() - - val converter = cryptoPortfolioCF.create(userWallet = userWallet) - - return converter.convertListBack(input = accounts) + return response } private suspend fun getFromLegacyStore(userWalletId: UserWalletId): UserTokensResponse? { return userTokensResponseStore.getSyncOrNull(userWalletId) + ?.let { + it.copy( + tokens = UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, tokens = it.tokens), + ) + } .also { userTokensResponseStore.clear(userWalletId) } } - - private fun UserTokensResponse?.orDefault(userWallet: UserWallet): UserTokensResponse { - if (this != null) return this - - return userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet = userWallet), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - } - - private fun GetWalletAccountsResponse?.orDefault( - userWalletId: UserWalletId, - accountDTOs: List, - userTokensResponse: UserTokensResponse, - ): GetWalletAccountsResponse { - if (this != null) return this - - return GetWalletAccountsResponse( - wallet = GetWalletAccountsResponse.Wallet( - group = userTokensResponse.group, - sort = userTokensResponse.sort, - totalAccounts = accountDTOs.size, - ), - accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens), - unassignedTokens = emptyList(), - ) - } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt index da4e0e7853..40e4fd5f47 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer +import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -35,10 +36,11 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow> { return userWalletsStore.userWallets + .map { it.map(UserWallet::walletId) } .distinctUntilChanged() - .flatMapLatest { userWallets -> + .flatMapLatest { ids -> combine( - flows = userWallets.map(walletAccountListFlowFactory::create), + flows = ids.map(walletAccountListFlowFactory::create), transform = ::listOf, ) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 43d0745ea2..6288d9036f 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.producer import arrow.core.Option import arrow.core.none -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -11,16 +10,13 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull /** * Default implementation of [SingleAccountListProducer]. * Produces a list of [AccountList] for a specific user wallet. * * @property params params containing the user wallet ID - * @property userWalletsStore store that provides user wallets * @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet * @property dispatchers coroutine dispatchers provider * @@ -28,7 +24,6 @@ import kotlinx.coroutines.flow.mapNotNull */ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @Assisted val params: SingleAccountListProducer.Params, - private val userWalletsStore: UserWalletsStore, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountListProducer { @@ -37,11 +32,7 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { - return userWalletsStore.userWallets - .mapNotNull { userWallets -> - userWallets.firstOrNull { it.walletId == params.userWalletId } - } - .flatMapLatest(walletAccountListFlowFactory::create) + return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) .flowOn(dispatchers.default) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 765d6afcc1..44aa8713fc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -4,9 +4,11 @@ import com.tangem.data.account.converter.AccountListConverter import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.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 kotlinx.coroutines.flow.* @@ -22,12 +24,15 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class WalletAccountListFlowFactory @Inject constructor( + private val userWalletsStore: UserWalletsStore, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val accountListConverterFactory: AccountListConverter.Factory, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) { - fun create(userWallet: UserWallet): Flow { + fun create(userWalletId: UserWalletId): Flow { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + return if (userWallet.isMultiCurrency) { createForMultiWallet(userWallet) } else { @@ -53,6 +58,6 @@ internal class WalletAccountListFlowFactory @Inject constructor( setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) } - return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies) + return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a9ceb3026e..524e256767 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -101,12 +101,12 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun saveAccounts(accountList: AccountList) { - val userWalletId = accountList.userWallet.walletId + val userWallet = userWalletsStore.getSyncStrict(accountList.userWalletId) - val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet) + val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) val accountsResponse = converter.convert(value = accountList) - walletAccountsSaver.pushAndStore(userWalletId = userWalletId, response = accountsResponse) + walletAccountsSaver.pushAndStore(userWalletId = userWallet.walletId, response = accountsResponse) } override suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option = option { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt new file mode 100644 index 0000000000..8dad68c1de --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -0,0 +1,68 @@ +package com.tangem.data.account.utils + +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +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.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject + +/** + * Factory to create default [GetWalletAccountsResponse]. + * + * @property userWalletsListRepository repository to get user wallet information + * @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts + * @property userTokensResponseFactory factory to create [UserTokensResponse] + * @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet + * +[REDACTED_AUTHOR] + */ +internal class DefaultWalletAccountsResponseFactory @Inject constructor( + private val userWalletsListRepository: UserWalletsListRepository, + private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, + private val userTokensResponseFactory: UserTokensResponseFactory, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, +) { + + suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse { + val userWallet = userWalletsListRepository.userWalletsSync().firstOrNull { it.walletId == userWalletId } + + val accountDTOs = userWallet?.let(::createDefaultAccountDTOs).orEmpty() + val response = userTokensResponse.orDefault(userWallet = userWallet) + + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = response.group, + sort = response.sort, + totalAccounts = accountDTOs.size, + ), + accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens), + unassignedTokens = emptyList(), + ) + } + + private fun createDefaultAccountDTOs(userWallet: UserWallet): List { + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + + val converter = cryptoPortfolioCF.create(userWallet = userWallet) + + return converter.convertListBack(input = accounts) + } + + private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse { + if (this != null) return this + + return userTokensResponseFactory.createUserTokensResponse( + currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt index 880b20520e..12ac6dda89 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -8,7 +8,6 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId internal fun createWalletAccountDTO( @@ -72,13 +71,13 @@ internal fun createGetWalletAccountsResponse( } internal fun createAccountList( - userWallet: UserWallet, + userWalletId: UserWalletId, sortType: TokensSortType = TokensSortType.BALANCE, groupType: TokensGroupType = TokensGroupType.NETWORK, ): AccountList { return AccountList( - userWallet = userWallet, - accounts = setOf(createCryptoPortfolio(userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(createCryptoPortfolio(userWalletId)), totalAccounts = 1, sortType = sortType, groupType = groupType, diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt index 8b32adb5e5..1a7ae063f0 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -96,7 +96,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ), @@ -110,7 +110,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), @@ -124,7 +124,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt index 766544b58a..2b09ada07f 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt @@ -46,7 +46,7 @@ class GetWalletAccountsResponseConverterTest { @Test fun `cryptoPortfolioConverter throws exception`() { // Arrange - val domain = createAccountList(userWallet = userWallet) + val domain = createAccountList(userWalletId = userWallet.walletId) val exception = IllegalStateException("Test exception") every { cryptoPortfolioConverter.convertBack(any()) } throws exception @@ -92,7 +92,7 @@ class GetWalletAccountsResponseConverterTest { return listOf( ConvertModel( value = createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ), @@ -106,7 +106,7 @@ class GetWalletAccountsResponseConverterTest { ), ConvertModel( value = createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt index 6d5ebee814..35e95170e1 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt @@ -2,14 +2,10 @@ package com.tangem.data.account.converter import com.google.common.truth.Truth import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse -import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.every -import io.mockk.mockk import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -19,13 +15,11 @@ class SaveWalletAccountsResponseConverterTest { @Test fun convert() { // Arrange - val userWallet = mockk { - every { this@mockk.walletId } returns UserWalletId("011") - } + val userWalletId = UserWalletId("011") val accountList = AccountList( - userWallet = userWallet, - accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)), totalAccounts = 1, ) .getOrNull()!! @@ -36,7 +30,7 @@ class SaveWalletAccountsResponseConverterTest { // Assert val expected = SaveWalletAccountsResponse( accounts = listOf( - WalletAccountDTO( + SaveWalletAccountsResponse.AccountDTO( id = accountList.mainAccount.accountId.value, name = (accountList.mainAccount.accountName as? AccountName.Custom)?.value, derivationIndex = accountList.mainAccount.derivationIndex.value, diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt index ac012e76fb..4494f5af61 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -5,6 +5,7 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse @@ -36,6 +37,7 @@ class DefaultWalletAccountsFetcherTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler = mockk(relaxUnitFun = true) + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true) private val fetcher: DefaultWalletAccountsFetcher = DefaultWalletAccountsFetcher( @@ -43,6 +45,7 @@ class DefaultWalletAccountsFetcherTest { accountsResponseStoreFactory = accountsResponseStoreFactory, userTokensSaver = userTokensSaver, fetchWalletAccountsErrorHandler = fetchWalletAccountsErrorHandler, + defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, eTagsStore = eTagsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -205,6 +208,16 @@ class DefaultWalletAccountsFetcherTest { tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag) } returns apiError as ApiResponse + coEvery { + fetchWalletAccountsErrorHandler.handle( + error = apiError.cause, + userWalletId = userWalletId, + savedAccountsResponse = null, + pushWalletAccounts = any(), + storeWalletAccounts = any(), + ) + } returns savedAccountsResponse + // Act fetcher.fetch(userWalletId) diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 422c9a7b8a..e8a3341536 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -1,9 +1,7 @@ package com.tangem.data.account.fetcher -import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.toUserTokensResponse -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code @@ -11,12 +9,11 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.account.models.AccountList -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -29,35 +26,21 @@ import org.junit.jupiter.api.TestInstance class FetchWalletAccountsErrorHandlerTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) - private val userWalletsStore: UserWalletsStore = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) - private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk() - private val cryptoPortfolioConverter = mockk() - private val userTokensResponseFactory: UserTokensResponseFactory = mockk() - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val handler = FetchWalletAccountsErrorHandler( userTokensSaver = userTokensSaver, - userWalletsStore = userWalletsStore, userTokensResponseStore = userTokensResponseStore, - cryptoPortfolioCF = cryptoPortfolioCF, - userTokensResponseFactory = userTokensResponseFactory, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, ) - private val userWallet = mockk { - every { this@mockk.walletId } returns userWalletId - } - @BeforeEach fun setupEach() { clearMocks( userTokensSaver, - userWalletsStore, userTokensResponseStore, - cryptoPortfolioCF, - cryptoPortfolioConverter, - cardCryptoCurrencyFactory, + defaultWalletAccountsResponseFactory, ) } @@ -84,12 +67,8 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify(inverse = true) { - userWalletsStore.getSyncStrict(key = any()) userTokensResponseStore.getSyncOrNull(userWalletId = any()) - userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - cryptoPortfolioCF.create(any()) - cryptoPortfolioConverter.convertListBack(any()) + defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any()) pushWalletAccounts(any(), any()) userTokensSaver.push(userWalletId = any(), response = any()) storeWalletAccounts(any(), any()) @@ -146,12 +125,8 @@ class FetchWalletAccountsErrorHandlerTest { } coVerify(inverse = true) { - userWalletsStore.getSyncStrict(key = any()) userTokensResponseStore.getSyncOrNull(userWalletId = any()) - userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - cryptoPortfolioCF.create(any()) - cryptoPortfolioConverter.convertListBack(any()) + defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any()) } } @@ -160,9 +135,6 @@ class FetchWalletAccountsErrorHandlerTest { // Arrange val error = ApiResponseError.TimeoutException() - val accounts = AccountList.empty(userWallet).accounts - .filterIsInstance() - val accountDTO = WalletAccountDTO( id = "nibh", name = "Michael Dotson", @@ -186,18 +158,10 @@ class FetchWalletAccountsErrorHandlerTest { val userTokensResponse = savedAccountsResponse.toUserTokensResponse() - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet - every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter - every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO) - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null - every { - userTokensResponseFactory.createUserTokensResponse( - currencies = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - } returns userTokensResponse - every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList() + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns userTokensResponse + coEvery { + defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse) + } returns savedAccountsResponse val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) @@ -213,16 +177,8 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify { - userWalletsStore.getSyncStrict(userWalletId) - cryptoPortfolioCF.create(userWallet) - cryptoPortfolioConverter.convertListBack(accounts) userTokensResponseStore.getSyncOrNull(userWalletId) - userTokensResponseFactory.createUserTokensResponse( - currencies = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) + defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse) storeWalletAccounts(userWalletId, any()) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt index 3ca26b0198..05cb95cca1 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt @@ -51,8 +51,8 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) // Act val actual = producer.produce().let(::getEmittedValues) @@ -63,7 +63,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -73,11 +73,11 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -95,9 +95,9 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -107,10 +107,10 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -128,9 +128,9 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -141,7 +141,7 @@ class DefaultMultiAccountListProducerTest { every { userWalletsStore.userWallets } returns userWalletsFlow val exception = RuntimeException("Converter error") - every { walletAccountListFlowFactory.create(userWallet) } throws exception + every { walletAccountListFlowFactory.create(userWalletId) } throws exception // Act val actual = producer.produceWithFallback().let(::getEmittedValues) @@ -152,7 +152,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -178,7 +178,7 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - every { walletAccountListFlowFactory.create(userWallet) } returns emptyFlow() + every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow() // Act val actual = producer.produce().let(::getEmittedValues) @@ -188,7 +188,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -203,9 +203,9 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) - every { walletAccountListFlowFactory.create(userWallet2) } returns emptyFlow() + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) + every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow() // Act val actual = producer.produce().let(::getEmittedValues) @@ -215,8 +215,8 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - walletAccountListFlowFactory.create(userWallet2) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId2) } } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt index 846e46bfea..bc065b4578 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.models.TokensSortType @@ -11,7 +10,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -26,7 +24,6 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultSingleAccountListProducerTest { - private val userWalletsStore: UserWalletsStore = mockk() private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk() private val userWalletId = UserWalletId("011") @@ -36,24 +33,22 @@ class DefaultSingleAccountListProducerTest { private val producer = DefaultSingleAccountListProducer( params = SingleAccountListProducer.Params(userWalletId = userWalletId), - userWalletsStore = userWalletsStore, walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @AfterEach fun tearDownEach() { - clearMocks(userWalletsStore, walletAccountListFlowFactory) + clearMocks(walletAccountListFlowFactory) } @Test fun produce() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + MutableStateFlow(listOf(userWallet)) - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) // Act val actual = producer.produce().let(::getEmittedValues) @@ -63,22 +58,18 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @Test fun `flow will updated if factoryFlow is updated`() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -95,23 +86,18 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(updatedAccountList) coVerifyOrder { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId) } } @Test fun `flow is filtered the same response`() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -128,71 +114,8 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(accountList) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId) } } - - @Test - fun `flow is empty if factory throws exception`() = runTest { - // Arrange - val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val exception = RuntimeException("Converter error") - every { walletAccountListFlowFactory.create(userWallet) } throws exception - - // Act - val actual = producer.produceWithFallback().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - } - } - - @Test - fun `flow is empty if userWalletsFlow returns empty flow`() = runTest { - // Arrange - val userWalletsFlow = emptyFlow>() - every { userWalletsStore.userWallets } returns userWalletsFlow - - // Act - val actual = producer.produce().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(exactly = 1) { userWalletsStore.userWallets } - coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } - } - - @Test - fun `flow is empty if userWalletsFlow doesn't contains userWalletId from params`() = runTest { - // Arrange - val unknownId = UserWalletId("012") - val unknownWallet = mockk { - every { this@mockk.walletId } returns unknownId - } - - val userWalletsFlow = MutableStateFlow(listOf(unknownWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - // Act - val actual = producer.produce().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - } - - coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } - } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index 6e13e22dad..47c2e8f6fc 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -10,6 +10,7 @@ import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -28,6 +29,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WalletAccountListFlowFactoryTest { + private val userWalletsStore: UserWalletsStore = mockk() private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk() private val accountsResponseStore: AccountsResponseStore = mockk() private val accountsResponseStoreFlow = MutableStateFlow(value = null) @@ -38,6 +40,7 @@ class WalletAccountListFlowFactoryTest { private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val factory = WalletAccountListFlowFactory( + userWalletsStore = userWalletsStore, accountsResponseStoreFactory = accountsResponseStoreFactory, accountListConverterFactory = accountListConverterFactory, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, @@ -49,6 +52,7 @@ class WalletAccountListFlowFactoryTest { @AfterEach fun tearDownEach() { clearMocks( + userWalletsStore, accountsResponseStoreFactory, accountsResponseStore, accountListConverterFactory, @@ -66,17 +70,19 @@ class WalletAccountListFlowFactoryTest { every { this@mockk.isMultiCurrency } returns true } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + val accountsResponse = createGetWalletAccountsResponse(userWalletId) every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore every { accountsResponseStore.data } returns accountsResponseStoreFlow accountsResponseStoreFlow.value = accountsResponse - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) every { accountListConverterFactory.create(userWallet) } returns accountListConverter every { accountListConverter.convert(accountsResponse) } returns accountList // Act - val actual = factory.create(userWallet).let(::getEmittedValues) + val actual = factory.create(userWalletId).let(::getEmittedValues) // Assert val expected = accountList @@ -99,14 +105,16 @@ class WalletAccountListFlowFactoryTest { fun `create for single wallet`() = runTest { val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) + every { userWalletsStore.getSyncStrict(userWallet.walletId) } returns userWallet + val currency = cryptoCurrencyFactory.ethereum every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency // Act - val actual = factory.create(userWallet).let(::getEmittedValues) + val actual = factory.create(userWallet.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWallet = userWallet, cryptoCurrencies = setOf(currency)) + val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency)) Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { @@ -126,16 +134,18 @@ class WalletAccountListFlowFactoryTest { fun `flow is created for single wallet with token`() = runTest { val nodl = MockUserWalletFactory.createSingleWalletWithToken() + every { userWalletsStore.getSyncStrict(nodl.walletId) } returns nodl + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() every { cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } returns currencies.toList() // Act - val actual = factory.create(nodl).let(::getEmittedValues) + val actual = factory.create(nodl.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWallet = nodl, cryptoCurrencies = currencies) + val expected = AccountList.empty(userWalletId = nodl.walletId, cryptoCurrencies = currencies) Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 1e7c97fc64..b830678839 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -584,7 +584,7 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.walletId } returns userWalletId } - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse @@ -593,6 +593,8 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.convert(accountList) } returns accountsResponse } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter @@ -617,7 +619,7 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.walletId } returns userWalletId } - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse @@ -626,6 +628,8 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.convert(accountList) } returns accountsResponse } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt new file mode 100644 index 0000000000..e270dacc98 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -0,0 +1,240 @@ +package com.tangem.data.account.utils + +import com.google.common.truth.Truth +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.converter.createWalletAccountDTO +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +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.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultWalletAccountsResponseFactoryTest { + + private val userWalletsListRepository = mockk() + private val cryptoPortfolioCF = mockk() + private val cryptoPortfolioConverter = mockk() + private val userTokensResponseFactory = mockk() + private val cardCryptoCurrencyFactory = mockk() + + private val factory = DefaultWalletAccountsResponseFactory( + userWalletsListRepository = userWalletsListRepository, + cryptoPortfolioCF = cryptoPortfolioCF, + userTokensResponseFactory = userTokensResponseFactory, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + ) + + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun setUpEach() { + every { cryptoPortfolioCF.create(any()) } returns cryptoPortfolioConverter + } + + @AfterEach + fun tearDownEach() { + clearMocks( + userWalletsListRepository, + cryptoPortfolioCF, + cryptoPortfolioConverter, + userTokensResponseFactory, + cardCryptoCurrencyFactory, + ) + } + + @Test + fun `create returns empty accounts when user wallet not found`() = runTest { + // Arrange + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + + coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList() + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns userTokensResponse + + // Act + val actual = factory.create(userWalletId = userWalletId, userTokensResponse = null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsListRepository.userWalletsSync() + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + } + + @Test + fun `create returns response with default tokens when userTokensResponse is null`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + + val defaultCoins = listOf(mockk()) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins + + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = listOf(mockk(relaxed = true)), + ) + + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = defaultCoins, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns defaultResponse + + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + + val accountsDTO = createWalletAccountDTO(userWalletId) + every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO) + + // Act + val actual = factory.create(userWalletId, null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = defaultResponse.group, + sort = defaultResponse.sort, + totalAccounts = 1, + ), + accounts = listOf(accountsDTO), + unassignedTokens = emptyList(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsListRepository.userWalletsSync() + cryptoPortfolioConverter.convertListBack(accounts) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) + userTokensResponseFactory.createUserTokensResponse( + currencies = defaultCoins, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + } + + @Test + fun `create returns response with default tokens when userTokensResponse is null and no default coins`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList() + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns defaultResponse + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList() + + // Act + val actual = factory.create(userWalletId, null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = defaultResponse.group, + sort = defaultResponse.sort, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `create returns response with assigned tokens`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + val assignedTokens = listOf(mockk(), mockk()) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = listOf(mockk(relaxed = true)), + ) + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = assignedTokens, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns userTokensResponse + + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + val accountsDTO = createWalletAccountDTO(userWalletId) + every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO) + + // Act + val actual = factory.create(userWalletId, userTokensResponse) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = userTokensResponse.group, + sort = userTokensResponse.sort, + totalAccounts = 1, + ), + accounts = listOf(accountsDTO), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index c7aff7add9..07463e771f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -326,6 +326,7 @@ class NetworkFactory @Inject constructor( Blockchain.Pepecoin, Blockchain.PepecoinTestnet, Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet, Blockchain.Quai, Blockchain.QuaiTestnet, + Blockchain.Linea, Blockchain.LineaTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index fe654114a6..2a67f6acfe 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -160,5 +160,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null Blockchain.Quai, Blockchain.QuaiTestnet -> null + Blockchain.Linea, Blockchain.LineaTestnet -> null } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index b005177a40..d0d1af52ab 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 @@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject private const val VALID_STATUS = "valid" +private const val APPROVED_KYC_STATUS = "APPROVED" private const val TAG = "TangemPay: OnboardingRepository" internal class DefaultOnboardingRepository @Inject constructor( @@ -47,13 +48,36 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun getMainScreenCustomerInfo(): Either { return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.requestWithPersistedToken { authHeader -> - tangemPayApi.getCustomerMe(authHeader) - }.result + val customerWalletAddress = requestHelper.getCustomerWalletAddress() - val orderStatus = getOrderStatus().getOrNull() ?: error("Order status is null") + when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) { + // If order id wasn't saved -> get customer info + null -> { + MainScreenCustomerInfo( + info = getCustomerInfoWithPersistedToken(), + orderStatus = OrderStatus.UNKNOWN, + ) + } + // If order id was saved -> check its status + else -> { + val orderStatus = getOrderStatus(orderId) + val customerInfo = when (orderStatus) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null) - MainScreenCustomerInfo(info = getCustomerInfo(result), orderStatus = orderStatus) + // Order was created/cancelled -> clear order id and get customer info + OrderStatus.UNKNOWN, + OrderStatus.COMPLETED, + OrderStatus.CANCELED, + -> getCustomerInfoWithPersistedToken().also { + tangemPayStorage.clearOrderId(customerWalletAddress) + } + } + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + } + } } } @@ -84,27 +108,28 @@ internal class DefaultOnboardingRepository @Inject constructor( } return CustomerInfo( productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) }, - kycStatus = response?.kyc?.status, + isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS, cardInfo = cardInfo, ) } - private suspend fun getOrderStatus(): Either { - return requestHelper.runWithErrorLogs(TAG) { - val walletAddress = requestHelper.getCustomerWalletAddress() - val orderId: String = tangemPayStorage.getOrderId(walletAddress) - ?: return@runWithErrorLogs OrderStatus.NOT_ISSUED + private suspend fun getOrderStatus(orderId: String): OrderStatus { + val result = requestHelper.request { authHeader -> + tangemPayApi.getOrder(authHeader, orderId) + }.result ?: error("Order result is null") - val result = requestHelper.request { authHeader -> - tangemPayApi.getOrder(authHeader, orderId) - }.result ?: error("Order result is null") - - when (result.status) { - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED - } + return when (result.status) { + OrderStatus.NEW.apiName -> OrderStatus.NEW + OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING + OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED + else -> OrderStatus.CANCELED } } + + private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo { + val result = requestHelper.requestWithPersistedToken { authHeader -> + tangemPayApi.getCustomerMe(authHeader) + }.result + return getCustomerInfo(result) + } } \ No newline at end of file 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 be43b2bd72..7e197fea52 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 @@ -86,6 +86,6 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( }.result val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull() txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) - } + }.onLeft { error(it.toString()) } } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt similarity index 51% rename from data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt rename to data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 8abd538e91..8d96150927 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -1,7 +1,9 @@ package com.tangem.data.yield.supply import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.yieldsupply.YieldSupplyProvider import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.local.yieldsupply.YieldMarketsStore @@ -9,8 +11,11 @@ import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.data.yield.supply.converters.YieldTokenStatusConverter import com.tangem.data.yield.supply.converters.YieldTokenChartConverter +import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData @@ -20,25 +25,29 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlin.collections.map -internal class DefaultYieldSupplyMarketRepository( +internal class DefaultYieldSupplyRepository( private val yieldSupplyApi: YieldSupplyApi, private val store: YieldMarketsStore, + private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, -) : YieldSupplyMarketRepository { +) : YieldSupplyRepository { override suspend fun getCachedMarkets(): List? = withContext(dispatchers.io) { - store.getSyncOrNull()?.enrichNetworkIds() + val cache = store.getSyncOrNull().orEmpty() + val domain = cache.map(YieldMarketTokenConverter::convert) + domain.enrichNetworkIds() } override suspend fun updateMarkets(): List = withContext(dispatchers.io) { - val response = yieldSupplyApi.getYieldMarkets().getOrThrow() + val chains = Blockchain.yieldSupplySupportedBlockchains().map { it.getChainId() }.joinToString(",") + val response = yieldSupplyApi.getYieldMarkets(chainId = chains).getOrThrow() val domain = response.marketDtos.map(YieldMarketTokenConverter::convert) - store.store(domain) + store.store(response.marketDtos) domain } override fun getMarketsFlow(): Flow> = store.get().map { - it.enrichNetworkIds() + it.map(YieldMarketTokenConverter::convert).enrichNetworkIds() } override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus { @@ -55,6 +64,41 @@ internal class DefaultYieldSupplyMarketRepository( return YieldTokenChartConverter.convert(response) } + override suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean = + withContext(dispatchers.io) { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = cryptoCurrency.network.toBlockchain(), + derivationPath = cryptoCurrency.network.derivationPath.value, + ) ?: error("Wallet manager not found") + + (walletManager as? YieldSupplyProvider)?.isSupported() ?: false + } + + override suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean = + withContext(dispatchers.io) { + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + ?: error("Chain id is required for evm's") + yieldSupplyApi.activateYieldModule( + YieldSupplyChangeTokenStatusBody( + tokenAddress = cryptoCurrencyToken.contractAddress, + chainId = chainId, + ), + ).getOrThrow().isActive + } + + override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean = + withContext(dispatchers.io) { + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + ?: error("Chain id is required for evm's") + yieldSupplyApi.deactivateYieldModule( + YieldSupplyChangeTokenStatusBody( + tokenAddress = cryptoCurrencyToken.contractAddress, + chainId = chainId, + ), + ).getOrThrow().isActive + } + private fun List.enrichNetworkIds(): List { val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() } return this.map { token -> diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 8888eba918..69ce23f67e 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -1,12 +1,12 @@ package com.tangem.data.yield.supply.di -import com.tangem.data.yield.supply.DefaultYieldSupplyMarketRepository +import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -37,12 +37,14 @@ internal object YieldSupplyDataModule { fun provideYieldSupplyMarketRepository( yieldSupplyApi: YieldSupplyApi, store: YieldMarketsStore, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, - ): YieldSupplyMarketRepository { - return DefaultYieldSupplyMarketRepository( + ): YieldSupplyRepository { + return DefaultYieldSupplyRepository( yieldSupplyApi = yieldSupplyApi, store = store, dispatchers = dispatchers, + walletManagersFacade = walletManagersFacade, ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 2f848f2f16..e305566047 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -8,14 +8,14 @@ import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable /** * Represents a list of accounts associated with a user wallet * - * @property userWallet the user wallet associated with the account list + * @property userWalletId the user wallet id associated with the account list * @property accounts a set of accounts belonging to the user wallet * @property totalAccounts the total number of accounts * @@ -23,7 +23,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class AccountList private constructor( - val userWallet: UserWallet, + val userWalletId: UserWalletId, val accounts: Set, val totalAccounts: Int, val sortType: TokensSortType, @@ -51,7 +51,7 @@ data class AccountList private constructor( val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId } return invoke( - userWallet = this.userWallet, + userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, sortType = this.sortType, @@ -73,7 +73,7 @@ data class AccountList private constructor( } return invoke( - userWallet = this.userWallet, + userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, sortType = this.sortType, @@ -134,12 +134,12 @@ data class AccountList private constructor( * Factory method to create an `AccountList` instance. * Validates the input to ensure the accounts list is not empty and contains exactly one main account. * - * @param userWallet the user wallet associated with the account list + * @param userWalletId the user wallet id associated with the account list * @param accounts a set of accounts belonging to the user wallet * @param totalAccounts the total number of accounts */ operator fun invoke( - userWallet: UserWallet, + userWalletId: UserWalletId, accounts: Set, totalAccounts: Int, sortType: TokensSortType = TokensSortType.NONE, @@ -169,7 +169,7 @@ data class AccountList private constructor( } AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = accounts, totalAccounts = totalAccounts, sortType = sortType, @@ -180,19 +180,19 @@ data class AccountList private constructor( /** * Factory method to create an empty [AccountList] with a main crypto portfolio account * - * @param userWallet the user wallet associated with the account list + * @param userWalletId the user wallet id associated with the account list */ fun empty( - userWallet: UserWallet, + userWalletId: UserWalletId, cryptoCurrencies: Set = emptySet(), sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): AccountList { return AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf( Account.CryptoPortfolio.createMainAccount( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, cryptoCurrencies = cryptoCurrencies, ), ), diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index 9581d1a6fb..4d3b4dab0e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -3,13 +3,13 @@ package com.tangem.domain.account.models import com.tangem.domain.models.TotalFiatBalance 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.models.wallet.UserWalletId import kotlinx.serialization.Serializable /** * Represents a list of account statuses associated with a user wallet * - * @property userWallet the user wallet to which the account statuses belong + * @property userWalletId the user wallet id to which the account statuses belong * @property accountStatuses a set of account statuses associated with the user wallet * @property totalAccounts the total number of accounts (including archived ones) * @property totalFiatBalance the total fiat balance across all accounts @@ -18,7 +18,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class AccountStatusList( - val userWallet: UserWallet, + val userWalletId: UserWalletId, val accountStatuses: Set, val totalAccounts: Int, val totalFiatBalance: TotalFiatBalance, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 0c43198fa0..60eea11794 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -8,11 +8,7 @@ import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.clearMocks -import io.mockk.mockk -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -31,7 +27,7 @@ class AccountListTest { val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) val accountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ) @@ -49,13 +45,13 @@ class AccountListTest { fun canAddMoreAccounts() { // Arrange val accountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 2), totalAccounts = 2, ).getOrNull()!! val fullAccountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! @@ -67,16 +63,13 @@ class AccountListTest { @Test fun empty() { - // Arrange - val userWallet = mockk(relaxed = true) - // Act - val actual = AccountList.empty(userWallet) + val actual = AccountList.empty(userWalletId) // Assert val expected = AccountList( - userWallet = userWallet, - accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)), totalAccounts = 1, ).getOrNull()!! @@ -87,19 +80,12 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Create { - private val userWallet = mockk() - - @BeforeEach - fun resetMocks() { - clearMocks(userWallet) - } - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: CreateTestModel) { // Act val actual = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = model.accounts, totalAccounts = model.accounts.size, ) @@ -131,13 +117,13 @@ class AccountListTest { createAccounts(userWalletId = userWalletId, count = 1).let { CreateTestModel( accounts = it, - expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1), + expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 1), ) }, createAccounts(userWalletId = userWalletId, count = 20).let { CreateTestModel( accounts = it, - expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20), + expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 20), ) }, CreateTestModel( @@ -171,8 +157,6 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Plus { - private val userWallet = mockk() - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: PlusTestModel) { @@ -191,13 +175,13 @@ class AccountListTest { PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, newAccount), totalAccounts = 2, ), @@ -211,13 +195,13 @@ class AccountListTest { PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(newAccount), totalAccounts = 1, ), @@ -226,7 +210,7 @@ class AccountListTest { // endregion PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!!, @@ -246,8 +230,6 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Minus { - private val userWallet = mockk() - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: MinusTestModel) { @@ -266,13 +248,13 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, secondaryAccount), totalAccounts = 2, ).getOrNull()!!, toRemove = secondaryAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ), @@ -286,13 +268,13 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toRemove = notInList, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ), @@ -305,7 +287,7 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, @@ -321,7 +303,7 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, secondaryAccount), totalAccounts = 2, ).getOrNull()!!, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index 4808e416a1..32e113b374 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -14,7 +14,6 @@ import com.tangem.domain.account.utils.createAccount import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -35,20 +34,16 @@ class AddCryptoPortfolioUseCaseTest { mainAccountTokensMigration = mainAccountTokensMigration, ) - private val userWallet = mockk() - @BeforeEach fun resetMocks() { - clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration, userWallet) - - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration) } @Test fun `invoke should add new crypto portfolio account to existing list`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! coEvery { @@ -152,7 +147,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return error if account list requirements not met`() = runTest { // Arrange val accountList = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! @@ -228,7 +223,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = IllegalStateException("Test error") @@ -266,7 +261,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return new account if migrate returns error`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = Exception("Migration error") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index fe67edc618..b23bd9bfe0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -11,7 +11,6 @@ import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -24,19 +23,17 @@ class ArchiveCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository) - private val userWallet = mockk() @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should archive existing crypto portfolio account`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! val accountId = account.accountId val updatedAccountList = (accountList - account).getOrNull()!! @@ -103,7 +100,7 @@ class ArchiveCryptoPortfolioUseCaseTest { @Test fun `invoke should return error if account not found`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val accountId = AccountId.forCryptoPortfolio( userWalletId = userWalletId, derivationIndex = DerivationIndex(1).getOrNull()!!, @@ -126,7 +123,7 @@ class ArchiveCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! val accountId = account.accountId val updatedAccountList = (accountList - account).getOrNull()!! diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index eb4e423c11..f5ede48207 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -28,19 +27,17 @@ class RecoverCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = RecoverCryptoPortfolioUseCase(crudRepository) - private val userWallet = mockk() @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should recover archived crypto portfolio account`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val archivedAccount = ArchivedAccount( accountId = account.accountId, name = account.accountName, @@ -122,7 +119,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if getArchivedAccount throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val exception = IllegalStateException("Test error") coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() @@ -146,7 +143,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if getArchivedAccount returns null`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None @@ -169,7 +166,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val archivedAccount = ArchivedAccount( accountId = account.accountId, name = account.accountName, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index 3d904d3117..724429e3d3 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -29,19 +28,15 @@ class UpdateCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) - private val userWallet = mockk() - @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should update crypto portfolio account with new name`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -66,7 +61,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new icon`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( @@ -94,7 +89,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new name and icon`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -123,7 +118,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if name and icon are null`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() @@ -144,7 +139,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -192,7 +187,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts does not contain accountId`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = AccountId.forCryptoPortfolio( userWalletId = userWalletId, derivationIndex = DerivationIndex(1).getOrNull()!!, @@ -217,7 +212,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if saveAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 1bc12d67f3..564baa96b8 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -25,12 +25,15 @@ dependencies { api(projects.domain.staking) api(projects.domain.tokens) + implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.timber) + implementation(tangemDeps.blockchain) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) 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 2e1d0ce365..f7cab3c413 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 @@ -1,5 +1,6 @@ package com.tangem.domain.account.status.di +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -31,7 +32,9 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton - fun provideGetAccountCurrencyStatusUseCase(): GetAccountCurrencyStatusUseCase { - return GetAccountCurrencyStatusUseCase() + fun provideGetAccountCurrencyStatusUseCase( + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + ): GetAccountCurrencyStatusUseCase { + return GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = singleAccountStatusListSupplier) } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 9cc97e0760..e8c9a00296 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 @@ -6,6 +6,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.lceContent import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensGroupType @@ -39,6 +40,7 @@ import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @Assisted private val params: SingleAccountStatusListProducer.Params, + private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory, private val dispatchers: CoroutineDispatcherProvider, @@ -58,8 +60,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo if (account.cryptoCurrencies.isEmpty()) { createEmptyAccountStatusFlow(account) } else { + val userWallet = userWalletsListRepository.userWalletsSync().first { + it.walletId == params.userWalletId + } + getAccountStatusFlow( - userWallet = accountList.userWallet, + userWallet = userWallet, account = account, groupType = accountList.groupType, sortType = accountList.sortType, @@ -71,7 +77,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo val balances = accountStatuses.map { it.tokenList.totalFiatBalance } AccountStatusList( - userWallet = accountList.userWallet, + userWalletId = accountList.userWalletId, accountStatuses = accountStatuses.toSet(), totalAccounts = accountList.totalAccounts, totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt index 5e5ef8f506..6177e2cde5 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt @@ -3,6 +3,8 @@ package com.tangem.domain.account.status.supplier import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Supplier that provides a single [AccountStatusList] for a specific user wallet. @@ -12,4 +14,10 @@ import com.tangem.domain.core.flow.FlowCachingSupplier abstract class SingleAccountStatusListSupplier( override val factory: SingleAccountStatusListProducer.Factory, override val keyCreator: (SingleAccountStatusListProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + operator fun invoke(userWalletId: UserWalletId): Flow { + val params = SingleAccountStatusListProducer.Params(userWalletId) + return this.invoke(params) + } +} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt index f038816b91..18abf00e6c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt @@ -117,7 +117,7 @@ class GetAccountCurrencyByAddressUseCase( .firstOrNull() return ensureNotNull(result) { - "No account found for network: $networkId in walletId: ${accountList.userWallet.walletId}" + "No account found for network: $networkId in walletId: ${accountList.userWalletId}" } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt index be08772f57..1846999f3d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt @@ -2,24 +2,110 @@ package com.tangem.domain.account.status.usecase import arrow.core.Option import arrow.core.none +import arrow.core.toOption +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer /** * Use case to retrieve the status of a specific cryptocurrency associated with an account. * + * @property singleAccountStatusListSupplier supplier to get the list of account statuses. + * [REDACTED_AUTHOR] */ -// TODO: Implement [REDACTED_JIRA] -class GetAccountCurrencyStatusUseCase { +class GetAccountCurrencyStatusUseCase( + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, +) { /** - * Invokes the use case to get the [AccountCryptoCurrencyStatus] for the given [currencyId]. + * Invokes the use case to get the status of a specific cryptocurrency for a given user wallet. * - * @param currencyId The ID of the cryptocurrency to look up. - * - * @return An [Option] containing the [AccountCryptoCurrencyStatus] if found, - * or [arrow.core.None] if not found or if any validation fails. + * @param userWalletId the ID of the user wallet. + * @param currency the cryptocurrency for which the status is to be retrieved. + * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. */ - suspend operator fun invoke(currencyId: CryptoCurrency.ID): Option = none() + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Option { + return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + } + + /** + * Invokes the use case to get the status of a specific cryptocurrency by its ID for a given user wallet and network. + * If the [network] is null, it searches across all accounts for the cryptocurrency. + * + * @param userWalletId the ID of the user wallet. + * @param currencyId the ID of the cryptocurrency. + * @param network the network associated with the cryptocurrency, can be null. + * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + network: Network?, + ): Option { + val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull( + params = SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return none() + + return accountStatusList.getExpectedAccountStatuses(network) + .asSequence() + .filterIsInstance() + .mapNotNull { accountStatus -> + val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId } + ?: return@mapNotNull null + + AccountCryptoCurrencyStatus(account = accountStatus.account, status = status) + } + .firstOrNull() + .toOption() + } + + /** + * Retrieves the expected account statuses based on the provided [network]. + * If the [network] is null, all account statuses are returned. + * If the network has a specific derivation index, it filters the accounts accordingly. + * + * @param network the network to filter accounts by, can be null. + * @return a set of [AccountStatus] that match the expected criteria. + */ + private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): Set { + val possibleAccountIndex = network?.getAccountIndexOrNull() + + return when (possibleAccountIndex) { + // currency can be in any account + null -> accountStatuses + // currency only in the main account + DerivationIndex.Main.value -> setOf(mainAccount) + // currency only in the account with specific derivation index or in the main account + else -> { + val accountStatus = accountStatuses.firstOrNull { + val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false + + cryptoPortfolio.derivationIndex.value == possibleAccountIndex + } + + setOfNotNull(accountStatus, mainAccount) + } + } + } + + private fun Network.getAccountIndexOrNull(): Int? { + val blockchain = Blockchain.fromNetworkId(networkId = rawId) ?: return null + val recognizer = AccountNodeRecognizer(blockchain) + + return recognizer.recognize(derivationPath)?.toInt() + } } \ No newline at end of file diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt similarity index 100% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt similarity index 91% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt index ca151e1478..6601e4c64e 100644 --- a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.StatusSource @@ -37,6 +38,7 @@ import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultSingleAccountStatusListProducerTest { + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory = mockk() @@ -47,6 +49,7 @@ class DefaultSingleAccountStatusListProducerTest { private val producer = DefaultSingleAccountStatusListProducer( params = SingleAccountStatusListProducer.Params(userWalletId), + userWalletsListRepository = userWalletsListRepository, singleAccountListSupplier = singleAccountListSupplier, cryptoCurrencyStatusesFlowFactory = cryptoCurrencyStatusesFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), @@ -60,7 +63,7 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow is mapped for user wallet id from params`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) every { singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) @@ -71,7 +74,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -92,8 +95,8 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow will updated if balances are updated`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.BALANCE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.BALANCE) val accountListFlow = MutableStateFlow(value = accountList) @@ -106,7 +109,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert (first emission) val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -125,7 +128,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert (second emission) val expected2 = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = updatedAccountList.mainAccount, @@ -147,7 +150,7 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow is filtered the same balance`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val accountListFlow = MutableStateFlow(value = accountList) every { @@ -155,7 +158,7 @@ class DefaultSingleAccountStatusListProducerTest { } returns accountListFlow val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -191,10 +194,12 @@ class DefaultSingleAccountStatusListProducerTest { // Arrange val cryptoCurrencyFactory = MockCryptoCurrencyFactory() val accountList = AccountList.empty( - userWallet = userWallet, + userWalletId = userWalletId, cryptoCurrencies = cryptoCurrencyFactory.ethereumAndStellar.toSet(), ) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) } returns flowOf(accountList) @@ -220,7 +225,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index 829ce7b3ec..34b63f7842 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -218,7 +218,7 @@ class GetAccountCurrencyByAddressUseCaseTest { }, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(multiUserWallet) + val accountList = AccountList.empty(userWalletId) every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet)) coEvery { @@ -253,7 +253,7 @@ class GetAccountCurrencyByAddressUseCaseTest { network = currency.network, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(userWallet = multiUserWallet, cryptoCurrencies = setOf(currency)) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency)) every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet)) coEvery { diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt new file mode 100644 index 0000000000..b8614cc704 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt @@ -0,0 +1,157 @@ +package com.tangem.domain.account.status.usecase + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.assertNone +import com.tangem.common.test.utils.assertSome +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.TokensSortType +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.account.DerivationIndex +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.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetAccountCurrencyStatusUseCaseTest { + + private val supplier = mockk() + private val useCase = GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = supplier) + + private val userWalletId = UserWalletId("011") + private val supplierParams = SingleAccountStatusListProducer.Params(userWalletId) + private val currency = MockCryptoCurrencyFactory().ethereum.let { + val derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/1") + + it.copy( + network = it.network.copy( + id = Network.ID(value = "ethereum", derivationPath = derivationPath), + derivationPath = derivationPath, + ), + ) + } + + @BeforeEach + fun setUp() { + clearMocks(supplier) + } + + @Test + fun `invoke returns None when supplier returns null`() = runTest { + // Arrange + coEvery { supplier.getSyncOrNull(supplierParams) } returns null + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns None when AccountList does not contain required currency id`() = runTest { + // Arrange + val accountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns Some if network is not null`() = runTest { + // Arrange + val mainAccountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val account = mockk(relaxed = true) { + every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk()) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns Some if network is null`() = runTest { + // Arrange + val account = mockk(relaxed = true) { + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt similarity index 100% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 0d983f457f..1d3fa41ced 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -212,6 +212,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1 Blockchain.Quai -> EllipticCurve.Secp256k1 Blockchain.QuaiTestnet -> EllipticCurve.Secp256k1 + Blockchain.Linea -> EllipticCurve.Secp256k1 + Blockchain.LineaTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt index 8f9f3f26ff..2dd73a1cb4 100644 --- a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt +++ b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt @@ -168,6 +168,8 @@ class Wallet2CardConfigTest { Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1, Blockchain.Quai to EllipticCurve.Secp256k1, Blockchain.QuaiTestnet to EllipticCurve.Secp256k1, + Blockchain.Linea to EllipticCurve.Secp256k1, + Blockchain.LineaTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt deleted file mode 100644 index 90378e19c2..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.tokens - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - object ManageAccess : SetArgs - object ReadAccess : SetArgs - } -} - -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ 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 9a5438f259..cb32317055 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 @@ -2,8 +2,6 @@ package com.tangem.domain.pay.model import java.math.BigDecimal -private const val APPROVED_KYC_STATUS = "APPROVED" - data class MainScreenCustomerInfo( val info: CustomerInfo, val orderStatus: OrderStatus, @@ -11,7 +9,7 @@ data class MainScreenCustomerInfo( data class CustomerInfo( val productInstance: ProductInstance?, - val kycStatus: String?, + val isKycApproved: Boolean, val cardInfo: CardInfo?, ) { @@ -26,6 +24,4 @@ data class CustomerInfo( val currencyCode: String, val customerWalletAddress: String, ) - - fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS } \ No newline at end of file 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 6cab68a6fd..6ae706a0e1 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,7 +1,7 @@ package com.tangem.domain.pay.model enum class OrderStatus(val apiName: String) { - NOT_ISSUED(""), + UNKNOWN(""), NEW("NEW"), PROCESSING("PROCESSING"), COMPLETED("COMPLETED"), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index c3f3df8d95..ce09a009a3 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -36,7 +36,8 @@ interface HotWalletPasswordRequester { * @param hotWalletId The ID of the hot wallet to authenticate with. * @param authMode Indicates whether the request is for authentication mode. * In auth mode user can be deleted after failed attempts. - * @param hasBiometry Indicates whether to show biometric authentication option. + * @param hasBiometry Indicates whether to show biometric authentication option to the user. + * Will be ignored if the device does not support biometry at the moment of the request. */ data class AttemptRequest( val hotWalletId: HotWalletId, diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt similarity index 54% rename from domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt rename to domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 5d26c82073..eceba08082 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -1,12 +1,13 @@ package com.tangem.domain.yield.supply import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import kotlinx.coroutines.flow.Flow -interface YieldSupplyMarketRepository { +interface YieldSupplyRepository { /** * Get cached yield markets or null if nothing cached yet. @@ -35,4 +36,25 @@ interface YieldSupplyMarketRepository { */ @Throws suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData + + suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean + + /** + * Activate yield protocol for the specified token. + * + * Returns whether the token is active after the operation completes. + * May throw on network/backend errors or if required chain id cannot be resolved. + */ + @Throws + suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean + + /** + * Deactivate yield protocol for the specified token. + * + * Returns whether the token is active after the operation completes + * (expected to be false when deactivation succeeds). May throw on + * network/backend errors or if required chain id cannot be resolved. + */ + @Throws + suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt new file mode 100644 index 0000000000..92b5787e7d --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyActivateUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either = Either.catch { + yieldSupplyRepository.activateProtocol(cryptoCurrencyToken) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index 9efcab1ea6..d93fcca1a1 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -1,6 +1,6 @@ package com.tangem.domain.yield.supply.usecase -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,11 +12,11 @@ import kotlinx.coroutines.flow.map * - value: APY as string */ class YieldSupplyApyFlowUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { operator fun invoke(): Flow> { - return yieldSupplyMarketRepository.getMarketsFlow() + return yieldSupplyRepository.getMarketsFlow() .map { yieldMarketTokenList -> yieldMarketTokenList.filter { it.isActive }.associate { token -> token.yieldSupplyKey to token.apy.toString() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt index a3d9200e44..d2dff0ea7f 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlin.collections.filter /** @@ -12,11 +12,11 @@ import kotlin.collections.filter * - value: APY as string */ class YieldSupplyApyUpdateUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(): Either> = Either.catch { - yieldSupplyMarketRepository.updateMarkets() + yieldSupplyRepository.updateMarkets() .filter { it.isActive } .associate { it.tokenAddress to it.apy.toString() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt new file mode 100644 index 0000000000..6449187071 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyDeactivateUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either = Either.catch { + yieldSupplyRepository.deactivateProtocol(cryptoCurrencyToken) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt index 6bb5070262..14f176b3ec 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt @@ -1,14 +1,14 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository class YieldSupplyGetApyUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(tokenAddress: String): Either = Either.catch { - val apys = yieldSupplyMarketRepository.getCachedMarkets() ?: yieldSupplyMarketRepository.updateMarkets() + val apys = yieldSupplyRepository.getCachedMarkets() ?: yieldSupplyRepository.updateMarkets() apys.first { it.tokenAddress == tokenAddress }.apy.toString() } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt index 06cb773d1c..3d1142fb98 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt @@ -2,15 +2,15 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData class YieldSupplyGetChartUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(cryptoCurrency: CryptoCurrency.Token): Either = Either.catch { - yieldSupplyMarketRepository.getTokenChart(cryptoCurrency) + yieldSupplyRepository.getTokenChart(cryptoCurrency) } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt index 3a6d689924..c76f4c8e71 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt @@ -2,14 +2,14 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus class YieldSupplyGetTokenStatusUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(token: CryptoCurrency.Token): Either = Either.catch { - yieldSupplyMarketRepository.getTokenStatus(token) + yieldSupplyRepository.getTokenStatus(token) } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt new file mode 100644 index 0000000000..4b7fc6e168 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyIsAvailableUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + return yieldSupplyRepository.isYieldSupplySupported(userWalletId, cryptoCurrency) + } +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt index 0dc73a77b7..22fae1a67e 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt @@ -28,7 +28,7 @@ interface PortfolioFetcher { val walletBalance: Lce, val accountsBalance: AccountStatusList, ) { - val userWallet: UserWallet get() = accountsBalance.userWallet + val userWalletId: UserWalletId get() = accountsBalance.userWalletId } sealed interface Mode { 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 2c0a24413a..2cf339e309 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 @@ -13,7 +13,10 @@ 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.usecase.ArchiveCryptoPortfolioUseCase +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.details.entity.AccountDetailsUM @@ -30,6 +33,7 @@ internal class AccountDetailsModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { private val params = paramsContainer.require() @@ -42,8 +46,11 @@ internal class AccountDetailsModel @Inject constructor( } private fun onManageTokensClick() { - // todo account add account param - router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS)) + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.SETTINGS, + portfolioId = PortfolioId(params.account.accountId), + ) + router.push(route) } private fun onArchiveAccountClick() { @@ -89,6 +96,8 @@ internal class AccountDetailsModel @Inject constructor( ) } } + val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId) + .getOrNull()?.isMultiCurrency ?: false return AccountDetailsUM( accountName = params.account.accountName.toUM().value, accountIcon = params.account.portfolioIcon.toUM(), @@ -96,6 +105,7 @@ internal class AccountDetailsModel @Inject constructor( onAccountEditClick = ::onEditAccountClick, onManageTokensClick = ::onManageTokensClick, archiveMode = archiveMode, + isManageTokensAvailable = isMultiCurrency, ) } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt index 3938e757fb..27ad494553 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -7,6 +7,7 @@ internal data class AccountDetailsUM( val accountName: TextReference, val accountIcon: CryptoPortfolioIconUM, val archiveMode: ArchiveMode, + val isManageTokensAvailable: Boolean, val onCloseClick: () -> Unit, val onAccountEditClick: () -> Unit, val onManageTokensClick: () -> Unit, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt index bd4a0e319f..e3708f495f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -22,7 +22,6 @@ import com.tangem.common.ui.R import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.common.ui.account.AccountRow import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -49,6 +48,7 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = ) Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxSize() .padding(horizontal = TangemTheme.dimens.spacing16) @@ -61,21 +61,22 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) - SpacerH16() AccountRow(state) - SpacerH16() - ManageTokensRow(state) + if (state.isManageTokensAvailable) { + ManageTokensRow(state) + } when (state.archiveMode) { is AccountDetailsUM.ArchiveMode.Available -> { - SpacerH16() - ArchiveAccountRow(state.archiveMode) - SpacerH(8.dp) - Text( - modifier = Modifier.padding(horizontal = 12.dp), - text = stringResourceSafe(R.string.account_details_archive_description), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) + Column { + ArchiveAccountRow(state.archiveMode) + SpacerH(8.dp) + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.account_details_archive_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } } AccountDetailsUM.ArchiveMode.None -> Unit } @@ -186,10 +187,12 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider(null) @@ -60,7 +62,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( it.copy( isShown = true, accessCode = "", - useBiometricVisible = attemptRequest.hasBiometry, + useBiometricVisible = attemptRequest.isBiometryButtonVisible(), onAccessCodeChange = ::onAccessCodeChange, ) } @@ -83,7 +85,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( it.copy( accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, - useBiometricVisible = currentRequest.hasBiometry, + useBiometricVisible = currentRequest.isBiometryButtonVisible(), ) } delay(timeMillis = 500) // Delay to show the wrong access code state @@ -212,6 +214,9 @@ internal class HotAccessCodeRequestModel @Inject constructor( dismiss() } + private suspend fun HotWalletPasswordRequester.AttemptRequest.isBiometryButtonVisible(): Boolean = + hasBiometry && canUseBiometryUseCase() + private fun dismissState() { uiState.update { it.copy(isShown = false) diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index e680fabf26..bff75e7fb5 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -2,21 +2,13 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.wallet.UserWalletId interface ManageTokensComponent : ComposableContentComponent { data class Params( val mode: ManageTokensMode, val source: ManageTokensSource, - ) { - constructor(userWalletId: UserWalletId?, source: ManageTokensSource) : this( - source = source, - mode = userWalletId - ?.let { ManageTokensMode.Wallet(userWalletId) } - ?: ManageTokensMode.None, - ) - } + ) interface Factory : ComponentFactory } \ No newline at end of file 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 3c34dd0f34..bdd3cdbc10 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 @@ -17,6 +17,13 @@ sealed interface ManageTokensMode { } sealed interface AddCustomTokenMode { - data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode + + val userWalletId: UserWalletId + get() = when (this) { + is Account -> accountId.userWalletId + is Wallet -> userWalletId + } + + data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode data class Account(val accountId: AccountId) : AddCustomTokenMode } \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 0506848d40..58cffdfd57 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -25,6 +25,8 @@ dependencies { implementation(projects.common.ui) /* Project - Domain */ + implementation(projects.domain.account.status) + implementation(projects.domain.account) implementation(projects.domain.card) implementation(projects.domain.legacy) implementation(projects.domain.manageTokens) @@ -35,6 +37,14 @@ dependencies { implementation(projects.domain.swap.models) implementation(projects.domain.notifications) + // region Project - Libs + implementation(projects.libs.crypto) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.blockchain) + // endregion + /* AndroidX */ implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 56323dcd9a..13fca6567a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -54,6 +54,7 @@ internal sealed class ManageTokensUM { isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, scrollToTop: StateEvent = this.scrollToTop, needToInteractWithColdWallet: Boolean = this is ManageContent && this.needToInteractWithColdWallet, + topBar: ManageTokensTopBarUM? = this.topBar, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -65,6 +66,7 @@ internal sealed class ManageTokensUM { isSavingInProgress = isSavingInProgress, scrollToTop = scrollToTop, needToInteractWithColdWallet = needToInteractWithColdWallet, + topBar = topBar, ) is ReadContent -> copy( search = search, @@ -72,6 +74,7 @@ internal sealed class ManageTokensUM { isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, scrollToTop = scrollToTop, + topBar = topBar, ) } } 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 3bb119e580..cadcf9f356 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 @@ -3,13 +3,22 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.account.toUM 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.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.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.AccountStatus import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent @@ -25,10 +34,12 @@ import com.tangem.features.managetokens.entity.item.SelectableItemUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,6 +49,8 @@ internal class CustomTokenSelectorModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, private val messageSender: UiMessageSender, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model() { @@ -146,9 +159,8 @@ internal class CustomTokenSelectorModel @Inject constructor( return derivationPaths } - private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List = when (mode) { - is AddCustomTokenMode.Account -> TODO("Account") - is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> + private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List { + return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) messageSender.send(message) @@ -168,7 +180,60 @@ internal class CustomTokenSelectorModel @Inject constructor( fun selectCustomDerivationPath(value: SelectedDerivationPath) { when (params) { is NetworkSelector -> return - is DerivationPathSelector -> params.onDerivationPathSelected(value) + is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) { + params.checkAccountDerivation(value) + } else { + params.onDerivationPathSelected(value) + } } } + + private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) = + modelScope.launch { + val accountName = derivationPath.id + ?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer) + ?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } } + ?.let { accountNode -> + fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount && + this.account.derivationIndex.value.toLong() == accountNode + + val accounts = singleAccountStatusListSupplier(mode.userWalletId) + .first().accountStatuses + val account = accounts.find { + when (it) { + is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() + } + } + val accountName = when (account) { + is AccountStatus.CryptoPortfolio -> account.account.accountName.toUM() + null -> null + } + accountName + } + + if (accountName == null) { + onDerivationPathSelected(derivationPath) + } else { + showAccountNameExist( + accountName = accountName.value, + onClick = { onDerivationPathSelected(derivationPath) }, + ) + } + } + + private fun showAccountNameExist(accountName: TextReference, onClick: () -> Unit) { + val firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = onClick, + ) + val dialogMessage = DialogMessage( + title = resourceReference(R.string.custom_token_another_account_dialog_title), + message = resourceReference( + R.string.custom_token_another_account_dialog_description, + wrappedList(accountName), + ), + firstActionBuilder = { firstAction }, + ) + messageSender.send(dialogMessage) + } } \ No newline at end of file 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 c34a6ba32d..74ee2da6c5 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 @@ -17,6 +17,7 @@ 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.status.supplier.SingleAccountStatusListSupplier import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent @@ -40,13 +41,14 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class ManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, manageTokensListManagerFactory: ManageTokensListManager.Factory, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, @@ -86,6 +88,7 @@ internal class ManageTokensModel @Inject constructor( modelScope.launch { manageTokensListManager.launchPagination(isCollapsed = true) } + checkIsSupportAddCustomTokens() } fun reloadList() { @@ -105,16 +108,34 @@ internal class ManageTokensModel @Inject constructor( } } + 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, + ) + ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent( + title = resourceReference(R.string.common_search_tokens), + onBackButtonClick = router::pop, + ) + } + + private fun manageContentTopBar() = ManageTokensTopBarUM.ManageContent( + title = resourceReference(id = R.string.main_manage_tokens), + onBackButtonClick = router::pop, + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_plus_24, + onClicked = ::navigateToAddCustomToken, + ), + ) + private fun createReadContentModel(): ManageTokensUM.ReadContent { return ManageTokensUM.ReadContent( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), - topBar = ManageTokensTopBarUM.ReadContent( - title = resourceReference(R.string.common_search_tokens), - onBackButtonClick = router::pop, - ), + topBar = getTopBarInitialState(), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), query = "", @@ -132,14 +153,7 @@ internal class ManageTokensModel @Inject constructor( isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), - topBar = ManageTokensTopBarUM.ManageContent( - title = resourceReference(id = R.string.main_manage_tokens), - onBackButtonClick = router::pop, - endButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_plus_24, - onClicked = ::navigateToAddCustomToken, - ), - ), + topBar = getTopBarInitialState(), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), query = "", @@ -174,6 +188,20 @@ internal class ManageTokensModel @Inject constructor( .launchIn(modelScope) } + private fun checkIsSupportAddCustomTokens() { + when (val mode = params.mode) { + is ManageTokensMode.Account -> modelScope.launch { + val mainAccount = singleAccountStatusListSupplier(mode.accountId.userWalletId).first().mainAccount + if (mode.accountId == mainAccount.account.accountId) { + state.update { it.copySealed(topBar = manageContentTopBar()) } + } + } + ManageTokensMode.None, + is ManageTokensMode.Wallet, + -> Unit // use init state + } + } + private fun updateItems(items: ImmutableList) { val updatedState = state.updateAndGet { state -> state.copySealed( 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 e7426ae87c..2b20edf084 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 @@ -57,6 +57,7 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.wallet.UserWalletId 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.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -442,20 +443,23 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider { /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt index fd77ee062f..eea5740b23 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -1,17 +1,17 @@ package com.tangem.features.onramp.swap.availablepairs -import androidx.compose.runtime.Composable +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel -import com.tangem.features.onramp.tokenlist.ui.TokenList +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.ui.onrampTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow @Stable internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( @@ -21,11 +21,11 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( private val model: AvailableSwapPairsModel = getOrCreateModel(params) - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() + override val uiState: StateFlow + get() = model.state - TokenList(state = state, modifier = modifier) + override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { + onrampTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt new file mode 100644 index 0000000000..9489858928 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.converters + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class LoadingAccountTokenItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio { + val (account, currencies) = value + + return TokensListItemUM.Portfolio( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(), + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..9ee61d4ba9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt @@ -0,0 +1,61 @@ +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 { + 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(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, cryptoCurrencies) -> + unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + }.toPersistentList(), + ) + }, + 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 d62cce58ae..8e19216bf3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,12 +1,15 @@ package com.tangem.features.onramp.swap.availablepairs.model -import arrow.core.getOrElse 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.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.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -15,6 +18,8 @@ import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.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.tokenlist.TokenList @@ -28,11 +33,13 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen 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.entity.AccountAvailabilityUM +import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -43,7 +50,7 @@ import javax.inject.Inject private typealias AvailablePairsState = Lce> -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -53,7 +60,10 @@ internal class AvailableSwapPairsModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getAvailablePairsUseCase: GetAvailablePairsUseCase, - private val getWalletsUseCase: GetWalletsUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + getWalletsUseCase: GetWalletsUseCase, ) : Model() { val state: StateFlow = tokenListUMController.state @@ -62,13 +72,17 @@ internal class AvailableSwapPairsModel @Inject constructor( private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val tokenListFlow = getTokenListUseCaseFlow() - + private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) init { - initializeSearchBarCallbacks() + if (accountsFeatureToggles.isFeatureEnabled) { + subscribeOnUpdateStateV2() + } else { + subscribeOnUpdateState() + } - subscribeOnUpdateState() + initializeSearchBarCallbacks() subscribeOnAvailablePairsUpdates() } @@ -79,12 +93,20 @@ internal class AvailableSwapPairsModel @Inject constructor( maybeTokenList.getOrElse( ifLoading = { it ?: TokenList.Empty }, ifError = { TokenList.Empty }, - ) - .flattenCurrencies() + ).flattenCurrencies() } .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) } + private fun getAccountListUseCaseFlow(): SharedFlow> { + return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId)) + .distinctUntilChanged() + .map { accountStatusList -> + accountStatusList.accountStatuses.toList() + }.flowOn(dispatchers.default) + .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) + } + private fun initializeSearchBarCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( @@ -130,6 +152,53 @@ internal class AvailableSwapPairsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnUpdateStateV2() { + combine( + flow = getAccountsAndModeFlow(), + flow2 = getAppCurrencyAndBalanceHidingFlow(), + flow3 = params.selectedStatus, + flow4 = searchManager.query, + flow5 = availablePairsByNetworkFlow + .map { it[params.selectedStatus.value?.toLeastTokenInfo()] } + .distinctUntilChanged(), + ) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState -> + val (accountList, isAccountsMode) = accountListAndMode + availablePairsState?.fold( + ifLoading = { + SetLoadingAccountTokenListTransformer( + appCurrency = appCurrencyAndBalanceHiding.first, + accountList = accountList, + isAccountsMode = isAccountsMode, + ) + }, + ifContent = { pairs -> + handleContentStateV2( + appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, + accountList = accountList, + selectedStatus = selectedStatus, + query = query, + availablePairs = pairs, + isAccountsMode = isAccountsMode, + ) + }, + ifError = { + handleErrorStateV2( + cause = it, + networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), + accountList = accountList, + ) + }, + ) ?: SetLoadingAccountTokenListTransformer( + appCurrency = appCurrencyAndBalanceHiding.first, + accountList = accountList, + isAccountsMode = isAccountsMode, + ) + } + .onEach(tokenListUMController::update) + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + private fun handleContentState( appCurrencyAndBalanceHiding: Pair, currencies: List, @@ -176,6 +245,53 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun handleContentStateV2( + appCurrencyAndBalanceHiding: Pair, + accountList: List, + selectedStatus: CryptoCurrencyStatus?, + query: String, + availablePairs: List, + isAccountsMode: Boolean, + ): TokenListUMTransformer { + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + + val filterByQueryAccountList = accountList.associate { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.account to accountStatus.tokenList.flattenCurrencies() + .filter { it.currency != selectedStatus?.currency } + .filterByQuery(query = query) + } + } + + if (availablePairs.isEmpty()) { + return SetNoAvailablePairsTransformerV2( + appCurrency = appCurrency, + accountList = filterByQueryAccountList, + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + isBalanceHidden = isBalanceHidden, + isAccountsMode = isAccountsMode, + ) + } + + return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { + SetNothingToFoundStateTransformerV2( + isBalanceHidden = isBalanceHidden, + emptySearchMessageReference = resourceReference( + id = R.string.action_buttons_swap_empty_search_message, + ), + ) + } else { + UpdateAccountTokenListTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), + isBalanceHidden = isBalanceHidden, + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + isAccountsMode = isAccountsMode, + ) + } + } + private fun handleErrorState( cause: Throwable, networkInfo: LeastTokenInfo?, @@ -193,6 +309,26 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private fun handleErrorStateV2( + cause: Throwable, + networkInfo: LeastTokenInfo?, + accountList: List, + ): SetErrorWarningTransformer { + return SetErrorWarningTransformer( + cause = cause, + onRefresh = { + modelScope.launch { + if (networkInfo != null) { + accountList.filterIsInstance() + .forEach { (_, currencies) -> + updateAvailablePairs(networkInfo, currencies.flattenCurrencies()) + } + } + } + }, + ) + } + private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { params.selectedStatus @@ -203,9 +339,19 @@ internal class AvailableSwapPairsModel @Inject constructor( val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest - val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest - - updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) + if (accountsFeatureToggles.isFeatureEnabled) { + val accountList = accountListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs( + networkInfo = networkInfo, + statuses = accountList.filterIsInstance() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + }.toSet().toList(), + ) + } else { + val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) + } } } } @@ -247,6 +393,14 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private fun getAccountsAndModeFlow(): Flow, Boolean>> { + return combine( + flow = accountListFlow.distinctUntilChanged(), + flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(), + transform = ::Pair, + ) + } + private fun onSearchQueryChange(newQuery: String) { if (state.value.searchBarUM.query == newQuery) return @@ -286,6 +440,29 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun Map>.filterByAvailability( + availablePairs: List, + ): List { + return map { (account, currencies) -> + AccountAvailabilityUM( + account = account, + currencyList = currencies.map { status -> + val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo()) + + val isAvailableToSwap = isAvailable && + status.value !is CryptoCurrencyStatus.MissedDerivation && + status.value !is CryptoCurrencyStatus.Unreachable && + !status.currency.isCustom + + AccountCurrencyUM( + cryptoCurrencyStatus = status, + isAvailable = isAvailableToSwap, + ) + }, + ) + } + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt new file mode 100644 index 0000000000..f0856222ba --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrencyStatus + +internal data class AccountAvailabilityUM( + val account: Account.CryptoPortfolio, + val currencyList: List, +) + +internal data class AccountCurrencyUM( + val isAvailable: Boolean, + val cryptoCurrencyStatus: CryptoCurrencyStatus, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt index 817e7218ba..0b159c1423 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.swap.entity +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference @@ -11,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference internal sealed interface ExchangeCardUM { /** Title reference */ - val titleReference: TextReference + val titleUM: TitleUM /** Remove button UI model */ val removeButtonUM: RemoveButtonUM? @@ -19,11 +21,11 @@ internal sealed interface ExchangeCardUM { /** * Empty state * - * @property titleReference title reference + * @property titleUM title reference * @property subtitleReference empty token subtitle reference */ data class Empty( - override val titleReference: TextReference, + override val titleUM: TitleUM, val subtitleReference: TextReference, ) : ExchangeCardUM { @@ -33,15 +35,29 @@ internal sealed interface ExchangeCardUM { /** * Filled * - * @property titleReference title reference + * @property titleUM title reference * @property removeButtonUM remove button UI model * @property tokenItemState token item state */ data class Filled( - override val titleReference: TextReference, + override val titleUM: TitleUM, override val removeButtonUM: RemoveButtonUM?, val tokenItemState: TokenItemState, ) : ExchangeCardUM data class RemoveButtonUM(val onClick: () -> Unit) + + @Immutable + sealed interface TitleUM { + + data class Text( + val title: TextReference, + ) : TitleUM + + data class Account( + val prefixText: TextReference, + val name: TextReference, + val icon: CryptoPortfolioIconUM, + ) : TitleUM + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt index 08c088f59e..036c81e855 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.entity.transformer import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer @@ -17,6 +18,8 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled internal class SelectFromTokenTransformer( private val selectedTokenItemState: TokenItemState, private val onRemoveClick: () -> Unit, + private val account: Account.CryptoPortfolio, + private val isAccountsMode: Boolean, ) : SwapSelectTokensUMTransformer { override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { @@ -24,6 +27,9 @@ internal class SelectFromTokenTransformer( exchangeFrom = prevState.exchangeFrom.toFilled( selectedTokenItemState = selectedTokenItemState, removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick), + account = account, + isAccountsMode = isAccountsMode, + isFromCurrency = true, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt index 433b28f5ff..f8798e8f75 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.entity.transformer import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer @@ -15,12 +16,19 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled */ internal class SelectToTokenTransformer( private val selectedTokenItemState: TokenItemState, + private val isAccountsMode: Boolean, + private val account: Account.CryptoPortfolio, ) : SwapSelectTokensUMTransformer { override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { return prevState.copy( exchangeFrom = prevState.exchangeFrom.hideRemoveButton(), - exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState), + exchangeTo = prevState.exchangeTo.toFilled( + selectedTokenItemState = selectedTokenItemState, + isAccountsMode = isAccountsMode, + account = account, + isFromCurrency = false, + ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt index 6daf403437..04615a8b92 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt @@ -1,14 +1,16 @@ package com.tangem.features.onramp.swap.entity.utils +import com.tangem.common.ui.account.toUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.entity.ExchangeCardUM /** Create empty exchange "from" card */ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty { return ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), ) } @@ -16,7 +18,7 @@ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty { /** Create empty exchange "to" card */ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty { return ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_to_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive), ) } @@ -29,10 +31,25 @@ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty { */ internal fun ExchangeCardUM.toFilled( selectedTokenItemState: TokenItemState, + account: Account.CryptoPortfolio, + isAccountsMode: Boolean, + isFromCurrency: Boolean, removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null, ): ExchangeCardUM.Filled { return ExchangeCardUM.Filled( - titleReference = titleReference, + titleUM = if (isAccountsMode) { + ExchangeCardUM.TitleUM.Account( + prefixText = if (isFromCurrency) { + resourceReference(R.string.common_from) + } else { + resourceReference(R.string.common_to) + }, + name = account.accountName.toUM().value, + icon = account.icon.toUM(), + ) + } else { + titleUM + }, tokenItemState = selectedTokenItemState, removeButtonUM = removeButtonUM, ) 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 5472e0e61c..e0e2d8e657 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,9 @@ 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.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.entity.SwapSelectTokensController @@ -32,6 +34,7 @@ internal class SwapSelectTokensModel @Inject constructor( private val router: Router, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, ) : Model() { val state: StateFlow = controller.state @@ -43,9 +46,12 @@ internal class SwapSelectTokensModel @Inject constructor( private val params = paramsContainer.require() + private var isAccountsMode: Boolean = false + init { controller.update { it.copy(onBackClick = ::onBackClick) } + subscribeOnAccountsMode() subscribeOnBalanceHidingSettings() } @@ -66,6 +72,11 @@ internal class SwapSelectTokensModel @Inject constructor( transformer = SelectFromTokenTransformer( selectedTokenItemState = selectedTokenItemState, onRemoveClick = ::onRemoveFromTokenClick, + isAccountsMode = isAccountsMode, + account = Account.CryptoPortfolio.createMainAccount( + userWalletId = params.userWalletId, + cryptoCurrencies = setOf(status.currency), + ), // todo account from from cryptocurrency ), ) } @@ -84,7 +95,16 @@ internal class SwapSelectTokensModel @Inject constructor( modelScope.launch { _toCurrencyStatus.value = status - controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) + controller.update( + transformer = SelectToTokenTransformer( + selectedTokenItemState = selectedTokenItemState, + isAccountsMode = isAccountsMode, + account = Account.CryptoPortfolio.createMainAccount( + userWalletId = params.userWalletId, + cryptoCurrencies = setOf(status.currency), + ), // todo account from from cryptocurrency + ), + ) // require some delay to show state with selected "from" and "to" tokens delay(timeMillis = 500) @@ -119,6 +139,16 @@ internal class SwapSelectTokensModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnAccountsMode() { + isAccountsModeEnabledUseCase() + .distinctUntilChanged() + .onEach { + isAccountsMode = it + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + private fun onBackClick() { analyticsEventHandler.send( event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt index da3b5ead84..4ee9b3eb0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -14,11 +14,14 @@ 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.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.common.ui.account.AccountLabel +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.rows.NetworkTitle @@ -46,13 +49,14 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif modifier = modifier .fillMaxWidth() .heightIn(min = 116.dp) - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), verticalArrangement = Arrangement.SpaceBetween, ) { - Title(titleReference = state.titleReference, removeButtonUM = state.removeButtonUM) + Title( + titleUM = state.titleUM, + removeButtonUM = state.removeButtonUM, + ) AnimatedContent( targetState = state, @@ -73,16 +77,39 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif } @Composable -private fun Title(titleReference: TextReference, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) { +private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) { NetworkTitle( title = { - Text( - text = titleReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.subtitle2, - ) + AnimatedContent( + titleUM, + ) { currentState -> + when (currentState) { + is ExchangeCardUM.TitleUM.Account -> Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = currentState.prefixText.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + AccountLabel( + name = currentState.name, + icon = currentState.icon, + iconSize = AccountIconSize.ExtraSmall, + nameStyle = TangemTheme.typography.subtitle2, + nameColor = TangemTheme.colors.text.tertiary, + ) + } + is ExchangeCardUM.TitleUM.Text -> Text( + text = currentState.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, + ) + } + } }, action = { RemoveButton(state = removeButtonUM) }, ) @@ -153,7 +180,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider override val values: Sequence = sequenceOf( ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), ), createFilled(removeButtonUM = null), @@ -162,7 +189,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled { return ExchangeCardUM.Filled( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), removeButtonUM = removeButtonUM, tokenItemState = TokenItemState.Content( id = "1", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt index bf873aa6f5..346d2b498f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -23,6 +24,7 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.TokenListUM /** * Swap select tokens @@ -39,7 +41,9 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent internal fun SwapSelectTokens( state: SwapSelectTokensUM, selectFromTokenListComponent: OnrampTokenListComponent, + selectFromTokenListState: TokenListUM, selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, modifier: Modifier = Modifier, ) { BackHandler(onBack = state.onBackClick) @@ -77,33 +81,33 @@ internal fun SwapSelectTokens( } if (state.exchangeFrom is ExchangeCardUM.Empty) { - item(key = "select_from", contentType = "select_from") { - selectFromTokenListComponent.Content( - modifier = Modifier - .padding(horizontal = 16.dp) - .animateItem(), + with(selectFromTokenListComponent) { + content( + uiState = selectFromTokenListState, + modifier = Modifier, ) } } if (state.exchangeFrom is ExchangeCardUM.Filled) { item(key = "exchange_to", contentType = "exchange_to") { - ExchangeCard( - state = state.exchangeTo, - isBalanceHidden = state.isBalanceHidden, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .animateItem(), - ) + if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { + ExchangeCard( + state = state.exchangeTo, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .animateItem(), + ) + } } if (state.exchangeTo is ExchangeCardUM.Empty) { - item(key = "select_to", contentType = "select_to") { - selectToTokenListComponent.Content( - modifier = Modifier - .padding(horizontal = 16.dp) - .animateItem(), + with(selectToTokenListComponent) { + content( + uiState = selectToTokenListState, + modifier = Modifier.padding(horizontal = 16.dp), ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt index c538d475cd..d35442f63a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt @@ -1,17 +1,17 @@ package com.tangem.features.onramp.tokenlist -import androidx.compose.runtime.Composable +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel -import com.tangem.features.onramp.tokenlist.ui.TokenList +import com.tangem.features.onramp.tokenlist.ui.onrampTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow @Stable internal class DefaultOnrampTokenListComponent @AssistedInject constructor( @@ -21,11 +21,11 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor( private val model: OnrampTokenListModel = getOrCreateModel(params) - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() + override val uiState: StateFlow + get() = model.state - TokenList(state = state, modifier = modifier) + override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { + onrampTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index 94ba06e247..4490a37ec3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -3,14 +3,15 @@ package com.tangem.features.onramp.tokenlist import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableListContentComponent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onramp.tokenlist.entity.OnrampOperation +import com.tangem.features.onramp.tokenlist.entity.TokenListUM /** Token list component that present list of token for multi-currency wallet */ @Stable -internal interface OnrampTokenListComponent : ComposableContentComponent { +internal interface OnrampTokenListComponent : ComposableListContentComponent { /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index 615823f4e4..3f88743a05 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -19,6 +19,19 @@ internal data class TokenListUM( val searchBarUM: SearchBarUM, val availableItems: ImmutableList, val unavailableItems: ImmutableList, + val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val warning: NotificationUM? = null, -) \ No newline at end of file +) + +internal sealed interface TokenListUMData { + data class AccountList( + val tokensList: ImmutableList, + ) : TokenListUMData + + data class TokenList( + val tokensList: ImmutableList, + ) : TokenListUMData + + data object EmptyList : TokenListUMData +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt index 80bb1d02f0..583697cca2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -31,6 +31,7 @@ internal class TokenListUMController @Inject constructor() { ), availableItems = persistentListOf(), unavailableItems = persistentListOf(), + tokensListData = TokenListUMData.EmptyList, isBalanceHidden = false, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt new file mode 100644 index 0000000000..48d7b7331d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus +import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter +import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class SetLoadingAccountTokenListTransformer( + appCurrency: AppCurrency, + private val accountList: List, + private val isAccountsMode: Boolean, +) : TokenListUMTransformer { + + private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency) + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountListItemConverter.convertList( + accountList.filterIsInstance(), + ).toPersistentList(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList( + account.tokenList.flattenCurrencies(), + ) + } + }.toPersistentList(), + ) + }, + warning = null, + ) + } +} \ No newline at end of file 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 e98b29bfaa..fcaa9d6147 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 @@ -5,6 +5,7 @@ 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 import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -26,9 +27,9 @@ internal class SetNothingToFoundStateTransformer( id = emptySearchMessageReference.hashCode(), text = emptySearchMessageReference, ).let(::add) - } - .toImmutableList(), + }.toImmutableList(), unavailableItems = persistentListOf(), + tokensListData = TokenListUMData.EmptyList, isBalanceHidden = isBalanceHidden, ) } 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 new file mode 100644 index 0000000000..dafa2ac842 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt @@ -0,0 +1,29 @@ +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 +import kotlinx.collections.immutable.toImmutableList + +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 = buildList { + TokensListItemUM.Text( + id = emptySearchMessageReference.hashCode(), + text = emptySearchMessageReference, + ).let(::add) + }.toImmutableList()), + 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 new file mode 100644 index 0000000000..f90f1e9794 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class UpdateAccountTokenItemConverter( + private val appCurrency: AppCurrency, + private val unavailableErrorText: TextReference, + onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, +) : Converter { + + private val availableConverter = OnrampTokenItemStateConverterFactory + .createAvailableItemConverter(appCurrency, onItemClick) + + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) + + override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio { + return TokensListItemUM.Portfolio( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = value.account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = value.currencyList.asSequence().map { (isAvailable, status) -> + if (isAvailable) { + availableConverter.convert(status) + } else { + unavailableConverter.convert(status) + } + }.map(TokensListItemUM::Token).toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt new file mode 100644 index 0000000000..5b13ba643c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt @@ -0,0 +1,64 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +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 UpdateAccountTokenListTransformer( + private val appCurrency: AppCurrency, + private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, + private val accountList: List, + private val isBalanceHidden: Boolean, + private val unavailableErrorText: TextReference, + private val warning: NotificationUM? = null, + private val isAccountsMode: Boolean, +) : TokenListUMTransformer { + + private val accountListItemConverter = UpdateAccountTokenItemConverter( + appCurrency = appCurrency, + onItemClick = onItemClick, + unavailableErrorText = unavailableErrorText, + ) + + private val availableConverter = OnrampTokenItemStateConverterFactory + .createAvailableItemConverter(appCurrency, onItemClick) + + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { (isAvailable, status) -> + if (isAvailable) { + availableConverter.convert(status) + } else { + unavailableConverter.convert(status) + } + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList(), + ) + }, + isBalanceHidden = isBalanceHidden, + warning = warning, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index f693f02e79..67f0c13e19 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -6,6 +6,7 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormatte import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -21,7 +22,13 @@ internal object OnrampTokenItemStateConverterFactory { ): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) }, + subtitleStateProvider = { + createSubtitleState( + status = it, + isAvailable = true, + text = stringReference(value = it.currency.symbol), + ) + }, subtitle2StateProvider = ::createSubtitle2State, fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) @@ -40,7 +47,13 @@ internal object OnrampTokenItemStateConverterFactory { isAvailable = false, ) }, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) }, + subtitleStateProvider = { + createSubtitleState( + status = it, + text = stringReference(value = it.currency.symbol), + isAvailable = false, + ) + }, subtitle2StateProvider = ::createSubtitle2State, fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false) @@ -48,12 +61,43 @@ internal object OnrampTokenItemStateConverterFactory { ) } - private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState { + fun createUnavailableItemConverterV2( + appCurrency: AppCurrency, + unavailableErrorText: TextReference, + ): TokenItemStateConverter { + return TokenItemStateConverter( + appCurrency = appCurrency, + iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, + titleStateProvider = { + TokenItemState.TitleState.Content( + text = stringReference(value = it.currency.name), + isAvailable = false, + ) + }, + subtitleStateProvider = { + createSubtitleState( + status = it, + isAvailable = false, + text = unavailableErrorText, + ) + }, + subtitle2StateProvider = ::createSubtitle2State, + fiatAmountStateProvider = { + createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false) + }, + ) + } + + private fun createSubtitleState( + status: CryptoCurrencyStatus, + isAvailable: Boolean, + text: TextReference, + ): TokenItemState.SubtitleState { return when (status.value) { CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading else -> { TokenItemState.SubtitleState.TextContent( - value = stringReference(value = status.currency.symbol), + value = text, isAvailable = isAvailable, ) } 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 ae9b3f2e32..a61e5837a8 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 @@ -6,6 +6,11 @@ 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.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 +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 @@ -13,6 +18,8 @@ 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 @@ -23,13 +30,11 @@ 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 +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.OnrampOperation -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.SetNothingToFoundStateTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.entity.* +import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -42,7 +47,9 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("LongParameterList") +typealias AccountCryptoList = Map> + +@Suppress("LargeClass", "LongParameterList") internal class OnrampTokenListModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -55,6 +62,9 @@ internal class OnrampTokenListModel @Inject constructor( 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() { val state: StateFlow = tokenListUMController.state @@ -71,8 +81,11 @@ internal class OnrampTokenListModel @Inject constructor( onActiveChange = ::onSearchBarActiveChange, ), ) - - subscribeOnUpdateState() + if (accountsFeatureToggles.isFeatureEnabled) { + subscribeOnUpdateStateV2() + } else { + subscribeOnUpdateState() + } } private fun subscribeOnUpdateState() { @@ -95,12 +108,7 @@ internal class OnrampTokenListModel @Inject constructor( if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = when (params.filterOperation) { - OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message - OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message - OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message - } - .let(::resourceReference), + emptySearchMessageReference = getEmptySearchMessageReference(), ) } else { val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) { @@ -134,6 +142,62 @@ internal class OnrampTokenListModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnUpdateStateV2() { + combine( + flow = singleAccountStatusListSupplier( + SingleAccountStatusListProducer.Params(params.userWalletId), + ).distinctUntilChanged(), + flow2 = getAppCurrencyAndBalanceHidingFlow(), + flow3 = isAccountsModeEnabledUseCase(), + flow4 = searchManager.query, + flow5 = hasRestrictionForSellFlow(), + ) { accountList, appCurrencyAndBalanceHiding, isAccountsMode, query, hasRestrictionForSell -> + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + val filterByQueryAccountList = accountList.filterAccountsByQuery(query) + + if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { + updateTokenListUM( + SetNothingToFoundStateTransformerV2( + isBalanceHidden = isBalanceHidden, + emptySearchMessageReference = getEmptySearchMessageReference(), + ), + ) + } else { + updateTokenListUM( + SetLoadingAccountTokenListTransformer( + appCurrency = appCurrency, + accountList = accountList.accountStatuses.toList(), + isAccountsMode = isAccountsMode, + ), + ) + updateTokenListUM( + UpdateAccountTokenListTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + accountList = filterByQueryAccountList.filterByAvailability(), + isBalanceHidden = isBalanceHidden, + unavailableErrorText = getUnavailableTokensHeaderReference(), + warning = getSellWarning( + hasRestrictionForSell = hasRestrictionForSell, + isInsufficientBalanceForSell = accountList.isInsufficientBalanceForSell(), + ), + isAccountsMode = isAccountsMode, + ), + ) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private fun getAppCurrencyAndBalanceHidingFlow(): Flow> { + return combine( + flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), + transform = ::Pair, + ) + } + private fun hasRestrictionForSellFlow(): Flow { return if (params.filterOperation == OnrampOperation.SELL) { getUserCountryUseCase().map { maybe -> @@ -154,25 +218,52 @@ internal class OnrampTokenListModel @Inject constructor( } } + private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean { + return if (params.filterOperation == OnrampOperation.SELL) { + (totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true + } else { + false + } + } + private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) { OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header }.let(::resourceReference) + private fun getEmptySearchMessageReference() = when (params.filterOperation) { + OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message + OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message + OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message + }.let(::resourceReference) + private fun updateTokenListUM(transformer: TokenListUMTransformer) { - tokenListUMController.update { prevState -> - transformer.transform(prevState).apply { - if (isFirstInitialization(prevState = prevState, newState = this)) { - params.onTokenListInitialized() + modelScope.launch { + tokenListUMController.update { prevState -> + transformer.transform(prevState).apply { + if (isFirstInitialization(prevState = prevState, newState = this)) { + params.onTokenListInitialized() + } } } } } + private fun getSellWarning(hasRestrictionForSell: Boolean, isInsufficientBalanceForSell: Boolean) = when { + hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction + isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling + else -> null + } + private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean { - return prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() && - (newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty()) + 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()) + } } private fun onSearchQueryChange(newQuery: String) { @@ -195,6 +286,16 @@ internal class OnrampTokenListModel @Inject constructor( ) } + private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence() + .associate { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> { + val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query) + accountStatus.account to filteredList + } + } + }.filter { (_, value) -> value.isNotEmpty() } + private fun List.filterByQuery(query: String): List { return filter { it.currency.name.contains(other = query, ignoreCase = true) || @@ -237,6 +338,49 @@ internal class OnrampTokenListModel @Inject constructor( } } + private suspend fun AccountCryptoList.filterByAvailability(): List { + return coroutineScope { + map { (account, currencies) -> + async { + AccountAvailabilityUM( + account = account, + currencyList = currencies.map { status -> + 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 + } + } + + val isTotalAvailable = + isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable + + AccountCurrencyUM( + cryptoCurrencyStatus = status, + isAvailable = isTotalAvailable, + ) + }, + ) + } + }.awaitAll() + } + } + private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean { return when (params.filterOperation) { OnrampOperation.BUY -> { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 27943a21c4..aefd164597 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -3,32 +3,34 @@ package com.tangem.features.onramp.tokenlist.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics 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.notifications.NotificationUM -import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.tokenlist.PortfolioListItem +import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList @@ -36,17 +38,21 @@ import kotlinx.collections.immutable.ImmutableList * Token list * * @param state state - * @param modifier modifier * [REDACTED_AUTHOR] */ -@Composable -internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { - Column(modifier) { - if (state.warning == null) { - SearchBar(searchBarUM = state.searchBarUM) - } else { - AnimatedContent(targetState = state.warning, label = "") { warning -> +internal fun LazyListScope.onrampTokenList(state: TokenListUM) { + val itemModifier = Modifier.padding(horizontal = 16.dp) + + if (state.warning == null) { + searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier) + } else { + item("NotificationsKey") { + AnimatedContent( + targetState = state.warning, + label = "", + modifier = itemModifier, + ) { warning -> when (warning) { is NotificationUM.Warning.OnrampErrorNotification -> { Notification( @@ -60,31 +66,45 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { } } } + } - if (state.availableItems.isNotEmpty()) { - SpacerH12() - ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - } + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - if (state.unavailableItems.isNotEmpty()) { - SpacerH12() - ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + when (val list = state.tokensListData) { + is TokenListUMData.AccountList -> list.tokensList.forEach { item -> + portfolioTokensList( + portfolio = item, + isBalanceHidden = state.isBalanceHidden, + ) } + is TokenListUMData.TokenList -> { + tokensList( + items = list.tokensList, + isBalanceHidden = state.isBalanceHidden, + ) + } + TokenListUMData.EmptyList -> Unit } } -@Composable -private fun SearchBar(searchBarUM: SearchBarUM) { - SearchBar( - state = searchBarUM, - colors = TangemSearchBarDefaults.secondaryTextFieldColors, - ) +private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { + item("SearchKey") { + SearchBar( + state = searchBarUM, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + modifier = modifier, + ) + } } -@Composable -private fun ItemsBlock(items: ImmutableList, isBalanceHidden: Boolean) { - items.fastForEachIndexed { index, item -> - key(item.id) { +private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> TokenListItem( state = item, isBalanceHidden = isBalanceHidden, @@ -92,13 +112,70 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: .roundedShapeItemDecoration( currentIndex = index, lastIndex = items.lastIndex, - addDefaultPadding = false, backgroundColor = TangemTheme.colors.background.primary, ) .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) .semantics { lazyListItemPosition = index }, ) - } + }, + ) +} + +internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + + portfolioItem( + portfolio = portfolio, + modifier = Modifier.padding(top = 8.dp), + isBalanceHidden = isBalanceHidden, + ) + if (!isExpanded) return + itemsIndexed( + items = tokens, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = indexWithHeader, + lastIndex = tokens.lastIndex.inc(), + backgroundColor = TangemTheme.colors.background.primary, + ) + .conditional(tokenIndex == tokens.lastIndex) { + Modifier.padding(bottom = 8.dp) + }, + ) + }, + ) +} + +private fun LazyListScope.portfolioItem( + portfolio: TokensListItemUM.Portfolio, + modifier: Modifier, + isBalanceHidden: Boolean, +) { + item( + key = "account-${portfolio.id}", + contentType = "account", + ) { + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = portfolio.tokens.lastIndex.inc(), + backgroundColor = TangemTheme.colors.background.primary, + ) + .then(modifier), + ) } } @@ -107,12 +184,12 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: @Composable private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) { TangemThemePreview { - TokenList( - state = state, - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.secondary) - .padding(16.dp), - ) + LazyColumn( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + ) { + onrampTokenList( + state = state, + ) + } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt index 684e6d0cc3..2f7e48ad75 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import kotlinx.collections.immutable.persistentListOf internal class PreviewTokenListUMProvider : PreviewParameterProvider { @@ -42,6 +43,7 @@ internal class PreviewTokenListUMProvider : PreviewParameterProvider StakeScreenSource.Info StakingStep.Amount -> StakeScreenSource.Amount - StakingStep.Confirmation -> StakeScreenSource.Confirmation + StakingStep.Success, + StakingStep.Confirmation, + -> StakeScreenSource.Confirmation StakingStep.Validators, StakingStep.RestakeValidator, StakingStep.RewardsValidators, 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 e1e93296cf..92676e87dd 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 @@ -87,7 +87,7 @@ internal class StakingStateController @Inject constructor( cryptoCurrencyBlockchainId = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), - amountState = AmountState.Empty(isRedesignEnabled = false), + amountState = AmountState.Empty, validatorState = StakingStates.ValidatorState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 5ba32182c8..a4d0229f85 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -42,6 +42,7 @@ internal class StakingStateRouter( StakingStep.Amount, -> showConfirmation() StakingStep.Confirmation -> showInitial() + StakingStep.Success -> appRouter.pop() } } @@ -65,6 +66,7 @@ internal class StakingStateRouter( } } StakingStep.Validators -> showConfirmation() + StakingStep.Success -> appRouter.pop() } } 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 ae13e50f62..56f8f60341 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 @@ -138,4 +138,5 @@ enum class StakingStep { RestakeValidator, Confirmation, Validators, + Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index 936fea5c1b..9bb2b5eac3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -46,10 +46,11 @@ internal class SetAmountDataTransformer( return prevState.copy( amountState = AmountStateConverter( clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, maxEnterAmount = maxEnterAmount, + appCurrency = appCurrencyProvider(), + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + isBalanceHidden = false, ).convert( AmountParameters( title = title, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index a5d35a5215..e51cdf8d61 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -25,7 +25,6 @@ internal class SetButtonsStateTransformer( val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), - prevButton = getPrevButton(prevState), extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, txUrl = txUrl, onTextClick = urlOpener::openUrl, @@ -64,18 +63,6 @@ internal class SetButtonsStateTransformer( ) } - private fun getPrevButton(prevState: StakingUiState): NavigationButton? { - return NavigationButton( - textReference = TextReference.EMPTY, - iconRes = R.drawable.ic_back_24, - isSecondary = true, - isIconVisible = true, - shouldShowProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onPrevClick, - ).takeIf { prevState.currentStep.isPrevButtonVisible() } - } - private fun getExtraButtons(prevState: StakingUiState): Pair { return NavigationButton( textReference = resourceReference(R.string.common_explore), @@ -111,7 +98,7 @@ internal class SetButtonsStateTransformer( resourceReference(R.string.common_stake) } } - + StakingStep.Success -> resourceReference(R.string.common_close) StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, @@ -125,21 +112,17 @@ internal class SetButtonsStateTransformer( val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data val amountState = amountState as? AmountState.Data return if (confirmationState != null && amountState != null) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - resourceReference(R.string.common_close) - } else { - when (actionType) { - is StakingActionCommonType.Enter -> { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - resourceReference(R.string.give_permission_title) - } else { - resourceReference(R.string.common_stake) - } + when (actionType) { + is StakingActionCommonType.Enter -> { + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { + resourceReference(R.string.give_permission_title) + } else { + resourceReference(R.string.common_stake) } - is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) - is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } + is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } } else { resourceReference(R.string.common_close) @@ -155,6 +138,7 @@ internal class SetButtonsStateTransformer( StakingStep.Amount -> clickIntents.onAmountEnterClick() StakingStep.Confirmation -> onConfirmationClick() StakingStep.RewardsValidators -> Unit + StakingStep.Success -> clickIntents.onBackClick() } } @@ -178,17 +162,6 @@ internal class SetButtonsStateTransformer( } } - private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) { - StakingStep.InitialInfo, - StakingStep.RewardsValidators, - StakingStep.RestakeValidator, - StakingStep.Confirmation, - StakingStep.Validators, - -> false - StakingStep.Amount, - -> true - } - private fun StakingUiState.isPrimaryButtonDisabled(): Boolean { val initialState = initialInfoState as? StakingStates.InitialInfoState.Data val hasNotStaking = initialState?.yieldBalance == InnerYieldBalanceState.Empty @@ -205,6 +178,7 @@ internal class SetButtonsStateTransformer( StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled StakingStep.RestakeValidator, StakingStep.Validators, + StakingStep.Success, -> true } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 7eb0589c34..6f745765ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.utils.transformer.Transformer @@ -17,6 +18,7 @@ internal class SetConfirmationStateCompletedTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( confirmationState = prevState.confirmationState.copyWrapped(), + currentStep = StakingStep.Success, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index d26cfd7e24..f36b37e184 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -229,10 +229,11 @@ internal class SetInitialDataStateTransformer( ) return AmountStateConverter( clickIntents = clickIntents, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrency = appCurrencyProvider(), iconStateConverter = iconStateConverter, maxEnterAmount = maxEnterAmount, + isBalanceHidden = false, ).convert( AmountParameters( title = stringReference(userWalletProvider().name), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index d16bb669e4..12b26f1cfc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -28,7 +29,7 @@ internal object SetTitleTransformer : Transformer { R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - + StakingStep.Success -> TextReference.EMPTY StakingStep.Confirmation -> { when (actionType) { is StakingActionCommonType.Enter -> resourceReference( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 42f7777f9f..7f3853dbbd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -14,12 +14,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData -import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.components.transactions.TransactionDoneTitle 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.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -30,7 +31,6 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock -import com.tangem.features.staking.impl.presentation.model.StakingClickIntents @Suppress("LongParameterList") @Composable @@ -60,7 +60,7 @@ internal fun StakingConfirmationContent( subtitle = resourceReference(R.string.staking_transaction_in_progress_text), ) } - AmountBlock( + AmountBlockV2( amountState = amountState, isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress, isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, 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 5170756b32..3e5d834e16 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 @@ -91,6 +91,7 @@ private fun StakingAppBar(uiState: StakingUiState) { val (backIcon, click) = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Confirmation, + StakingStep.Success, -> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick StakingStep.Validators, StakingStep.RewardsValidators, @@ -108,6 +109,7 @@ private fun StakingAppBar(uiState: StakingUiState) { ) } +@Suppress("LongMethod") @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { val currentScreen = uiState.currentStep @@ -136,10 +138,10 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M contentAlignment = Alignment.TopCenter, label = "Staking Screen Navigation", transitionSpec = { - val direction = if (initialState.ordinal < targetState.ordinal) { - AnimatedContentTransitionScope.SlideDirection.Start - } else { - AnimatedContentTransitionScope.SlideDirection.End + val direction = when { + targetState == StakingStep.Success -> AnimatedContentTransitionScope.SlideDirection.Up + initialState.ordinal < targetState.ordinal -> AnimatedContentTransitionScope.SlideDirection.Start + else -> AnimatedContentTransitionScope.SlideDirection.End } slideIntoContainer(towards = direction, animationSpec = tween()) @@ -163,7 +165,6 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M } StakingStep.Amount -> AmountScreenContent( amountState = uiState.amountState, - isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, modifier = Modifier.background(TangemTheme.colors.background.secondary), ) @@ -173,6 +174,12 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, ) + StakingStep.Success -> StakingSuccessContent( + amountState = uiState.amountState, + state = uiState.confirmationState, + validatorState = uiState.validatorState, + clickIntents = uiState.clickIntents, + ) StakingStep.RestakeValidator, StakingStep.Validators, -> StakingValidatorListContent( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt new file mode 100644 index 0000000000..d9297f768d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.staking.impl.presentation.ui + +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.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +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.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock +import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock +import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock + +@Suppress("LongParameterList") +@Composable +internal fun StakingSuccessContent( + amountState: AmountState, + state: StakingStates.ConfirmationState, + validatorState: StakingStates.ValidatorState, + clickIntents: StakingClickIntents, +) { + if (state !is StakingStates.ConfirmationState.Data) return + val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED + val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } + Column( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + TransactionDoneTitle( + title = resourceReference(R.string.common_in_progress), + subtitle = resourceReference(R.string.staking_transaction_in_progress_text), + ) + AmountBlock( + amountState = amountState, + isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress, + isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, + onClick = clickIntents::onPrevClick, + ) + ValidatorBlock( + validatorState = validatorState, + isClickable = !isTransactionInProgress, + onClick = clickIntents::openValidators, + ) + StakingFeeBlock(feeState = state.feeState) + NotificationsBlock(notifications = state.notifications) + Spacer(Modifier) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_StakingConfirmationContent() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.primary)) { + StakingConfirmationContent( + amountState = AmountStatePreviewData.amountState, + state = ConfirmationStatePreviewData.assentStakingState, + validatorState = ValidatorStatePreviewData.validatorState, + clickIntents = StakingClickIntentsStub, + ) + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index b2d95fed03..62eb8a0d3b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -64,19 +64,13 @@ sealed class SwapAmountFieldUM { data class Empty( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty( - isPrimaryButtonEnabled = false, - isRedesignEnabled = true, - ) + override val amountField: AmountState = AmountState.Empty } data class Loading( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty( - isPrimaryButtonEnabled = false, - isRedesignEnabled = true, - ) + override val amountField: AmountState = AmountState.Empty } data class Content( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index c8eb091285..a490422d2f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -1,7 +1,7 @@ package com.tangem.features.swap.v2.impl.amount.model.converter import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.common.ui.amountScreen.converters.AmountStateConverterV2 +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.core.ui.components.atoms.text.TextEllipsis @@ -44,7 +44,7 @@ internal class SwapAmountFieldConverter( subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length), priceImpact = null, isClickEnabled = selectedType.isViewingField(), - amountField = AmountStateConverterV2( + amountField = AmountStateConverter( clickIntents = clickIntents, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index 52fc0217cd..b6b0ccec59 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -46,9 +46,7 @@ internal class SwapAmountBalanceHiddenTransformer( val newData = recalculatedPrimary.amountField newData.copy( amountTextField = oldData.amountTextField, - selectedButton = oldData.selectedButton, isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled, - isSegmentedButtonsEnabled = oldData.isSegmentedButtonsEnabled, isEditingDisabled = oldData.isEditingDisabled, reduceAmountBy = oldData.reduceAmountBy, isIgnoreReduce = oldData.isIgnoreReduce, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index bc921fd483..54177a3435 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -127,7 +127,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock( AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, availableBalanceCrypto = TextReference.EMPTY, ) ?: amountUM.secondaryAmount.amountField, isClickDisabled = true, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 3e6da5d36b..352cd998fa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -97,9 +97,7 @@ internal data object SwapAmountContentPreview { val defaultState = SwapAmountUM.Content( primaryAmount = SwapAmountFieldUM.Content( amountType = SwapAmountType.From, - amountField = AmountStatePreviewData.amountState.copy( - availableBalance = stringReference("Balance: 100 BTC"), - ), + amountField = AmountStatePreviewData.amountState, title = stringReference("Tether"), subtitleLeft = stringReference("11 101,123123456 BTC"), subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"), @@ -112,7 +110,6 @@ internal data object SwapAmountContentPreview { amountType = SwapAmountType.To, amountField = AmountStatePreviewData.amountState.copy( title = stringReference("Amount to receive"), - availableBalance = TextReference.EMPTY, ), title = stringReference("Shiba Inu"), priceImpact = stringReference("(-10%)"), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt index b430960c14..ba9bbc588f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt @@ -21,6 +21,7 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor companion object { val loadingUM = TangemPayTxHistoryUM.Loading(isBalanceHidden = true) val emptyUM = TangemPayTxHistoryUM.Empty(isBalanceHidden = true) + val errorUM = TangemPayTxHistoryUM.Error(isBalanceHidden = true, onReload = {}) val contentUM = TangemPayTxHistoryUM.Content( isBalanceHidden = false, loadMore = { false }, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt new file mode 100644 index 0000000000..50816ac2a7 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt @@ -0,0 +1,29 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.tangempay.details.impl.R + +internal sealed class TangemPayEmptyTransactionHistoryState { + + abstract val iconRes: Int + abstract val text: TextReference + + data class FailedToLoad( + private val onReload: () -> Unit, + ) : TangemPayEmptyTransactionHistoryState() { + override val iconRes: Int = R.drawable.ic_alert_history_64 + override val text: TextReference = resourceReference(R.string.transaction_history_error_failed_to_load) + val actionButtonConfig = ActionButtonConfig( + text = resourceReference(R.string.common_reload), + iconResId = R.drawable.ic_refresh_24, + onClick = onReload, + ) + } + + data object Empty : TangemPayEmptyTransactionHistoryState() { + override val iconRes: Int = R.drawable.ic_empty_token_64 + override val text: TextReference = resourceReference(R.string.transaction_history_empty_transactions) + } +} \ No newline at end of file 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 1b3778feb0..ac67e1d5b4 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 @@ -54,11 +54,16 @@ internal class TangemPayTxHistoryModel @Inject constructor( .onEach(::updateState) .launchIn(modelScope) listManager.paginationStatus - .onEach { paginationStatus -> handlePaginationStatus(paginationStatus) } + .onEach(::handlePaginationStatus) + .launchIn(modelScope) + listManager.emptyStatus + .onEach(::handleEmptyState) .launchIn(modelScope) } private fun updateState(items: ImmutableList) { + if (items.isEmpty()) return // fast exit. If items is empty, no need to update ui items + uiState.update { state -> if (state is TangemPayTxHistoryUM.Content) { state.copy(items = items) @@ -72,6 +77,12 @@ internal class TangemPayTxHistoryModel @Inject constructor( } } + private fun handleEmptyState(isEmpty: Boolean) { + if (isEmpty) { + uiState.update { getEmptyState(it.isBalanceHidden) } + } + } + private fun handlePaginationStatus(status: PaginationStatus<*>) { uiState.update { state -> when (status) { @@ -108,6 +119,10 @@ internal class TangemPayTxHistoryModel @Inject constructor( Timber.d("onTransactionClick: $item") } + private fun getEmptyState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Empty { + return TangemPayTxHistoryUM.Empty(isBalanceHidden = isBalanceHidden) + } + private fun getErrorState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Error { return TangemPayTxHistoryUM.Error(isBalanceHidden = isBalanceHidden, onReload = ::reload) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index c2e6cc25f7..bd9cc78a83 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -2,12 +2,7 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -15,12 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,21 +35,14 @@ import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.orMaskWithStars -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.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TokenDetailsTopBarTestTags import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent import com.tangem.features.tangempay.details.impl.R -import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM -import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState -import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig -import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.features.tangempay.entity.* import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf @@ -415,8 +399,8 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi ) } -@Preview(device = Devices.PIXEL_7_PRO, group = "day") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO, group = "night") +@Preview(device = Devices.PIXEL_7_PRO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO) @Composable private fun TangemPayDetailsScreenPreview( @PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM, @@ -473,4 +457,26 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + PreviewTangemPayTxHistoryComponent.loadingUM, + PreviewTangemPayTxHistoryComponent.contentUM, + PreviewTangemPayTxHistoryComponent.emptyUM, + PreviewTangemPayTxHistoryComponent.errorUM, + ), ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt index b7c07cbc03..466c8de420 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt @@ -20,6 +20,7 @@ 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.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension @@ -27,6 +28,7 @@ import coil.compose.rememberAsyncImagePainter import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -34,7 +36,9 @@ import com.tangem.core.ui.extensions.orMaskWithStars 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.EmptyTransactionBlockTestTags import com.tangem.core.ui.test.TransactionHistoryBlockTestTags +import com.tangem.features.tangempay.entity.TangemPayEmptyTransactionHistoryState import com.tangem.features.tangempay.entity.TangemPayTransactionState import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM @@ -43,12 +47,25 @@ private const val LOAD_ITEMS_BUFFER = 20 internal fun LazyListScope.tangemPayTxHistoryItems(listState: LazyListState, state: TangemPayTxHistoryUM) { when (state) { is TangemPayTxHistoryUM.Content -> contentItems(listState = listState, state = state) - is TangemPayTxHistoryUM.Empty -> TODO("[REDACTED_JIRA]") - is TangemPayTxHistoryUM.Error -> TODO("[REDACTED_JIRA]") + is TangemPayTxHistoryUM.Empty -> nonContentItem(state = TangemPayEmptyTransactionHistoryState.Empty) + is TangemPayTxHistoryUM.Error -> nonContentItem( + state = TangemPayEmptyTransactionHistoryState.FailedToLoad(onReload = state.onReload), + ) is TangemPayTxHistoryUM.Loading -> loadingItems(state = state) } } +private fun LazyListScope.nonContentItem(state: TangemPayEmptyTransactionHistoryState, modifier: Modifier = Modifier) { + item(key = state::class.java, contentType = state::class.java) { + TangemPayEmptyTransactionBlock( + state = state, + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } +} + private fun LazyListScope.contentItems(listState: LazyListState, state: TangemPayTxHistoryUM.Content) { itemsIndexed( items = state.items, @@ -370,4 +387,49 @@ private fun Timestamp(state: TangemPayTransactionState, modifier: Modifier = Mod ) } } +} + +@Composable +private fun TangemPayEmptyTransactionBlock( + state: TangemPayEmptyTransactionHistoryState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing24) + .testTag(EmptyTransactionBlockTestTags.BLOCK), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .testTag(EmptyTransactionBlockTestTags.ICON), + painter = painterResource(id = state.iconRes), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + + Text( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing32) + .testTag(EmptyTransactionBlockTestTags.TEXT), + textAlign = TextAlign.Center, + text = state.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + + when (state) { + is TangemPayEmptyTransactionHistoryState.Empty -> Unit + is TangemPayEmptyTransactionHistoryState.FailedToLoad -> ActionButton( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth(), + config = state.actionButtonConfig, + ) + } + } } \ No newline at end of file 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 3ccf9cf202..40cd31c7da 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 @@ -33,6 +33,7 @@ internal class TangemPayTxHistoryListManager( private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions) val uiItems: Flow> = uiManager.items + val emptyStatus: Flow = state.map { it.isEmpty }.distinctUntilChanged() val paginationStatus: Flow> = state.map { it.status }.distinctUntilChanged() suspend fun launchPagination() = coroutineScope { @@ -80,6 +81,7 @@ internal class TangemPayTxHistoryListManager( newCurrencyBatches = batchListState.data, clearUiBatches = clearUiBatches, ), + isEmpty = batchListState.status is PaginationStatus.EndOfPagination && batchListState.data.isEmpty(), ) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt index 5717238dda..5df7a11936 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt @@ -7,4 +7,5 @@ import com.tangem.pagination.PaginationStatus internal data class TangemPayTxHistoryState( val status: PaginationStatus<*> = PaginationStatus.None, val uiBatches: List>> = listOf(), + val isEmpty: Boolean = false, ) \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 65caca46f0..4b41785218 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -56,7 +56,7 @@ internal class TangemPayOnboardingModel @Inject constructor( repository.getCustomerInfo() .onRight { customerInfo -> when { - !customerInfo.isKycApproved() -> { + !customerInfo.isKycApproved -> { when (params) { is TangemPayOnboardingComponent.Params.Deeplink -> screenState.value = screenState.value.copy(fullScreenLoading = false) 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 1428366edc..bab7b098d3 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 @@ -17,7 +17,7 @@ import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen -import com.tangem.feature.tester.presentation.accounts.viewmodel.AccountsViewModel +import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen @@ -155,7 +155,7 @@ internal class TesterActivity : ComposeActivity() { } composable(route = TesterScreen.ACCOUNTS.name) { - val viewModel = hiltViewModel().apply { + val viewModel = hiltViewModel().apply { setupNavigation(innerTesterRouter) } val state by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt index 168afd1186..64cf095f9c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt @@ -9,9 +9,8 @@ internal data class AccountsUM( val onBackClick: () -> Unit, val walletSelector: WalletSelector, val accountListBottomSheetConfig: AccountListBottomSheetConfig, - val onAccountsClick: () -> Unit, + val onAccountsClick: () -> Boolean, val onFetchAccountsClick: () -> Unit, - val onCreateMainAccountClick: () -> Unit, val onClearETagClick: () -> Unit, ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt index 66202d3842..8ee0fd82be 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt @@ -62,8 +62,9 @@ internal fun AccountsScreen(state: AccountsUM, modifier: Modifier = Modifier) { ManageAccountsButtons( state = state, onAccountsClick = { context -> - if (state.accountListBottomSheetConfig.accounts.isNotEmpty()) { - state.onAccountsClick() + val isEmpty = state.onAccountsClick() + + if (!isEmpty) { isAccountListShown = true } else { Toast.makeText(context, "No accounts found", Toast.LENGTH_SHORT).show() @@ -242,16 +243,4 @@ private fun LazyListScope.ManageAccountsButtons(state: AccountsUM, onAccountsCli .fillMaxWidth(), ) } - - if (state.accountListBottomSheetConfig.accounts.none { it.isMainAccount }) { - item { - PrimaryButton( - text = "Create Main account", - onClick = state.onCreateMainAccountClick, - 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/accounts/viewmodel/AccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt similarity index 80% rename from features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt rename to features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 31be606a60..0824ae8aaa 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -6,13 +6,9 @@ import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.tester.presentation.accounts.entity.AccountsUM @@ -28,11 +24,10 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel -internal class AccountsViewModel @Inject constructor( +internal class TesterAccountsViewModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListFetcher: SingleAccountListFetcher, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsCRUDRepository: AccountsCRUDRepository, private val eTagsStore: ETagsStore, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel() { @@ -88,7 +83,6 @@ internal class AccountsViewModel @Inject constructor( ), onAccountsClick = ::updateAccountsList, onFetchAccountsClick = ::fetchAccounts, - onCreateMainAccountClick = ::createMainAccount, onClearETagClick = ::clearETag, ) } @@ -107,8 +101,8 @@ internal class AccountsViewModel @Inject constructor( } } - private fun updateAccountsList() { - val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return + private fun updateAccountsList(): Boolean { + val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return false val accounts = walletAccounts.value[userWalletId]?.accounts ?.filterIsInstance() @@ -122,6 +116,8 @@ internal class AccountsViewModel @Inject constructor( ), ) } + + return accounts.isEmpty() } private fun fetchAccounts() { @@ -134,28 +130,6 @@ internal class AccountsViewModel @Inject constructor( } } - private fun createMainAccount() { - viewModelScope.launch { - val userWallet = uiState.value.walletSelector.selected ?: return@launch - - // It's temporary solution to create main account for testing purposes - val accountList = AccountList( - userWallet = userWallet, - accounts = setOf( - Account.CryptoPortfolio.createMainAccount(userWallet.walletId).copy( - accountName = AccountName.invoke(value = "Main Account").getOrNull()!!, - ), - ), - totalAccounts = 1, - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ) - .getOrNull()!! - - accountsCRUDRepository.saveAccounts(accountList) - } - } - private fun clearETag() { viewModelScope.launch { val userWallet = uiState.value.walletSelector.selected ?: return@launch 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 e7864d0edc..d0674d5ed0 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 @@ -22,6 +22,7 @@ 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.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -81,6 +82,7 @@ internal class WalletSettingsModel @Inject constructor( private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -173,6 +175,7 @@ internal class WalletSettingsModel @Inject constructor( isUpgradeNotificationEnabled: Boolean, accountList: List, ): PersistentList { + val accountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled val isMultiCurrency = when (userWallet) { is UserWallet.Cold -> userWallet.isMultiCurrency is UserWallet.Hot -> true @@ -188,7 +191,7 @@ internal class WalletSettingsModel @Inject constructor( is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup is UserWallet.Hot -> false }, - isManageTokensAvailable = isMultiCurrency, + isManageTokensAvailable = !accountsFeatureEnabled && isMultiCurrency, isNFTFeatureEnabled = isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 6d2bf36754..e3caaf89d5 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -207,7 +208,7 @@ internal class ItemsBuilder @Inject constructor( iconRes = R.drawable.ic_tether_24, onClick = { analyticsEventHandler.send(Settings.ButtonManageTokens) - router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) + router.push(AppRoute.ManageTokens(Source.SETTINGS, PortfolioId(userWalletId))) }, ).let(::add) } 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 c843cb1153..c2d9b76a0c 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 @@ -359,10 +359,10 @@ internal class WalletModel @Inject constructor( val info = tangemPayMainScreenCustomerInfoUseCase() if (info != null) { stateHolder.update( - transformer = TangemPayStateTransformer( + transformer = TangemPayInitialStateTransformer( value = info, - onIssueOrderClick = ::issueOrder, - onContinueKycClick = innerWalletRouter::openTangemPayOnboarding, + onClickIssue = ::issueOrder, + onClickKyc = innerWalletRouter::openTangemPayOnboarding, openDetails = innerWalletRouter::openTangemPayDetails, ), ) @@ -371,9 +371,9 @@ internal class WalletModel @Inject constructor( private fun issueOrder() { modelScope.launch { - stateHolder.update(TangemPayStateTransformer(issueProgressState = true)) + stateHolder.update(TangemPayIssueProgressStateTransformer()) tangemPayIssueOrderUseCase().onLeft { - stateHolder.update(TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder)) + stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder)) } } } 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 eb07bc02f7..39e5b37251 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 @@ -4,17 +4,15 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account 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.isLocked import com.tangem.domain.nft.analytics.NFTAnalyticsEvent -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -39,15 +37,13 @@ internal interface WalletContentClickIntents { fun onDetailsClick() - fun onManageTokensClick() - fun onOrganizeTokensClick() fun onDismissMarketsOnboarding() - fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) + fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) - fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onAccountExpandClick(account: Account) @@ -81,7 +77,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, private val walletEventSender: WalletEventSender, private val analyticsEventHandler: AnalyticsEventHandler, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -122,11 +117,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onManageTokensClick() { - reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) - router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) - } - override fun onOrganizeTokensClick() { router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } @@ -138,13 +128,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) { - router.openTokenDetails(portfolioId, currencyStatus) + override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { + router.openTokenDetails(userWalletId, currencyStatus) } - override fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { - val userWalletId = portfolioId.userWalletId val userWallet = getUserWalletUseCase(userWalletId).getOrElse { Timber.e( """ @@ -160,7 +149,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) .take(count = 1) .collectLatest { - showActionsBottomSheet(it, userWallet, portfolioId) + showActionsBottomSheet(it, userWallet) } } } @@ -175,17 +164,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId) } - private fun showActionsBottomSheet( - tokenActionsState: TokenActionsState, - userWallet: UserWallet, - portfolioId: PortfolioId, - ) { + private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { stateHolder.showBottomSheet( ActionsBottomSheetConfig( actions = MultiWalletCurrencyActionsConverter( userWallet = userWallet, clickIntents = currencyActionsClickIntents, - portfolioId = portfolioId, ).convert(tokenActionsState), ), userWallet.walletId, 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 f57694c824..c2c173c0e9 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 @@ -29,7 +29,6 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency @@ -43,12 +42,7 @@ 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.stakekit.Yield -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase -import com.tangem.domain.tokens.NeedShowYieldSupplyDepositedWarningUseCase -import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase -import com.tangem.domain.tokens.SaveViewedYieldSupplyWarningUseCase -import com.tangem.domain.tokens.model.details.TokenAction +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.TokenReceiveAnalyticsEvent @@ -57,6 +51,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText +import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase @@ -70,7 +65,6 @@ 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.feature.wallet.presentation.wallet.utils.WalletFeatureUseCasesFacade import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -84,7 +78,7 @@ import javax.inject.Inject interface WalletCurrencyActionsClickIntents { fun onSendClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) @@ -92,32 +86,32 @@ interface WalletCurrencyActionsClickIntents { fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) fun onBuyClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - portfolioId: PortfolioId, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onReceiveClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null, ) - fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) + fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? - fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onExploreClick() @@ -139,7 +133,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val useCasesFacade: WalletFeatureUseCasesFacade, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, @@ -158,10 +151,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -185,27 +180,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSend(cryptoCurrencyStatus, portfolioId) + navigateToSend(cryptoCurrencyStatus, userWalletId) } }, ) } else { - navigateToSend(cryptoCurrencyStatus, portfolioId) + navigateToSend(cryptoCurrencyStatus, userWalletId) } } } override fun onReceiveClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?, ) { - val userWalletId = portfolioId.userWalletId - if (portfolioId is PortfolioId.Account) { - // todo account find address - TODO("account") - } - analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( token = cryptoCurrencyStatus.currency.symbol, @@ -278,12 +267,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) } - override fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { - val userWalletId = portfolioId.userWalletId - if (portfolioId is PortfolioId.Account) { - // todo account find address - TODO("account") - } + override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( token = cryptoCurrencyStatus.currency.symbol, @@ -305,7 +289,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), ) @@ -313,19 +297,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { walletEventSender.send( event = WalletEvent.ShowAlert( - state = getHideTokeAlertConfig(portfolioId, cryptoCurrencyStatus), + state = getHideTokeAlertConfig(userWalletId, cryptoCurrencyStatus), ), ) } } private suspend fun getHideTokeAlertConfig( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): WalletAlertState.DefaultAlert { val currency = cryptoCurrencyStatus.currency val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && - !useCasesFacade.isCryptoCurrencyCoinCouldHide(portfolioId = portfolioId, cryptoCurrencyCoin = currency) + !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) return if (isCryptoCurrencyCoinCouldHide) { WalletAlertState.DefaultAlert( title = resourceReference( @@ -351,14 +335,14 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), ), message = resourceReference(R.string.token_details_hide_alert_message), - onConfirmClick = { onPerformHideToken(portfolioId, cryptoCurrencyStatus) }, + onConfirmClick = { onPerformHideToken(userWalletId, cryptoCurrencyStatus) }, ) } } - override fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - useCasesFacade.removeCurrencyUseCase(portfolioId, cryptoCurrencyStatus.currency) + removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) .fold( ifLeft = { walletEventSender.send( @@ -366,7 +350,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) }, ) } @@ -399,7 +383,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onBuyClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -415,7 +399,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Onramp( - portfolioId = portfolioId, + userWalletId = userWalletId, currency = cryptoCurrencyStatus.currency, source = OnrampSource.TOKEN_LONG_TAP, ), @@ -424,7 +408,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - portfolioId: PortfolioId, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -447,12 +431,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSwap(cryptoCurrencyStatus, portfolioId) + navigateToSwap(cryptoCurrencyStatus, userWalletId) } }, ) } else { - navigateToSwap(cryptoCurrencyStatus, portfolioId) + navigateToSwap(cryptoCurrencyStatus, userWalletId) } } } @@ -493,15 +477,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + override fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) modelScope.launch { val cryptoCurrency = cryptoCurrencyStatus.currency appRouter.push( AppRoute.Staking( - portfolioId = portfolioId, + userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, yieldId = yield?.id ?: return@launch, ), @@ -519,15 +503,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return - val tokenListState = selectedWallet.tokensListState as? WalletTokensListState.ContentState.Content ?: return + val tokenListState = selectedWallet.tokensListState - if (tokenListState.items.count { it is TokensListItemUM.Token } < 2) { - handleError( - alertState = WalletAlertState.InsufficientTokensCountForSwapping, - eventCreator = MainScreenAnalyticsEvent::ButtonSwap, + when (tokenListState) { + is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability( + tokenCount = tokenListState.items.count { it is TokensListItemUM.Token }, ) - - return + is WalletTokensListState.ContentState.PortfolioContent -> checkSwapCryptoAvailability( + tokenCount = tokenListState.items.sumOf { it.tokens.count { it is TokensListItemUM.Token } }, + ) + WalletTokensListState.ContentState.Loading, + WalletTokensListState.ContentState.Locked, + WalletTokensListState.Empty, + -> return } modelScope.launch { @@ -766,21 +754,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } - private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) val route = AppRoute.Send( currency = cryptoCurrencyStatus.currency, - portfolioId = portfolioId, + userWalletId = userWalletId, ) appRouter.push(route) } - private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) { + private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { appRouter.push( AppRoute.Swap( currencyFrom = cryptoCurrencyStatus.currency, - portfolioId = portfolioId, + userWalletId = userWalletId, screenSource = AnalyticsParam.ScreensSources.LongTap.value, ), ) @@ -808,4 +796,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) } } + + private fun checkSwapCryptoAvailability(tokenCount: Int) { + if (tokenCount < 2) { + handleError( + alertState = WalletAlertState.InsufficientTokensCountForSwapping, + eventCreator = MainScreenAnalyticsEvent::ButtonSwap, + ) + + return + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index d1e7c8cba5..937ae92eea 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 @@ -365,7 +365,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletId = userWallet.walletId, currency = cryptoCurrency, source = OnrampSource.SEPA_BANNER, - launchSepa = true, + shouldLaunchSepa = true, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index cd5f108efd..50569e441a 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 @@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarCon 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( @@ -86,15 +87,17 @@ internal object WalletScreenPreviewData { private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( items = persistentListOf( TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance(), + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), isExpanded = false, - state = AccountItemPreviewData.accountItem + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem .copy(iconState = AccountItemPreviewData.accountLetterIcon), ), TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance(), + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), isExpanded = true, - state = AccountItemPreviewData.accountItem, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, ), ), organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( 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 95c0458d24..f2f41882cc 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 @@ -3,11 +3,9 @@ package com.tangem.feature.wallet.presentation.router import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRoute.ManageTokens.Source 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.PortfolioId import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -67,12 +65,12 @@ internal class DefaultWalletRouter @Inject constructor( urlOpener.openUrl(url) } - override fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) { + override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { val networkAddress = currencyStatus.value.networkAddress if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) { router.push( AppRoute.CurrencyDetails( - portfolioId = portfolioId, + userWalletId = userWalletId, currency = currencyStatus.currency, ), ) @@ -87,10 +85,6 @@ internal class DefaultWalletRouter @Inject constructor( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openManageTokensScreen(userWalletId: UserWalletId) { - router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) - } - override fun openScanFailedDialog(onTryAgain: () -> Unit) { reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } 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 2ff741c7b5..4c6c7ba002 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 @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -42,7 +41,7 @@ internal interface InnerWalletRouter { fun openUrl(url: String) /** Open token details screen */ - fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) + fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) /** Open stories screen */ fun openStoriesScreen() @@ -50,9 +49,6 @@ internal interface InnerWalletRouter { /** Is wallet last screen */ fun isWalletLastScreen(): Boolean - /** Open manage tokens screen */ - fun openManageTokensScreen(userWalletId: UserWalletId) - /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index bf5b27d5ea..fe583f7183 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -14,7 +14,6 @@ import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance 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.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -38,33 +37,41 @@ internal class TokenListAnalyticsSender @Inject constructor( private val mutex = Mutex() private val loadingTraces = mutableMapOf() - suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) { + suspend fun send( + displayedUiState: WalletState?, + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + flattenCurrencies: List, + ) { if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return - if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) { - startLoadingTraceIfNeeded(userWallet.walletId, tokenList) + if (totalFiatBalance is TotalFiatBalance.Loading) { + startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies) return } - if (isTerminalState(tokenList.totalFiatBalance)) { - stopLoadingTraceIfNeeded(userWallet.walletId, tokenList.totalFiatBalance) + if (isTerminalState(totalFiatBalance)) { + stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance) } - val currenciesStatuses = tokenList.flattenCurrencies() + val currenciesStatuses = flattenCurrencies - sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses) - sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses) + sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses) + sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses) sendUnreachableNetworksEventIfNeeded(currenciesStatuses) sendTokenBalancesIfNeeded(currenciesStatuses) } - private suspend fun startLoadingTraceIfNeeded(userWalletId: UserWalletId, tokenList: TokenList) { + private suspend fun startLoadingTraceIfNeeded( + userWalletId: UserWalletId, + flattenCurrencies: List, + ) { mutex.withLock { if (!loadingTraces.containsKey(userWalletId)) { val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME) trace.start() - trace.putAttribute(TOKENS_COUNT, tokenList.flattenCurrencies().size.toString()) + trace.putAttribute(TOKENS_COUNT, flattenCurrencies.size.toString()) loadingTraces[userWalletId] = trace } } 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 2d5684a5c0..be35057603 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 @@ -9,17 +9,16 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.account.models.AccountStatusList 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.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.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.onramp.GetOnrampCountryUseCase @@ -45,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject @@ -70,72 +70,81 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - val tokenListFlow = if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { + val accountStatusList by lazy { val params = SingleAccountStatusListProducer.Params(userWallet.walletId) accountDependencies.singleAccountStatusListSupplier(params) - } else { - tokenListStore.getOrThrow(userWallet.walletId) + .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( - tokenListFlow, + // todo account just use it, after delete accountsFeatureToggles + // accountStatusListFlow, isReadyToShowRateAppUseCase(), isNeedToBackupUseCase(userWallet.walletId), seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa), notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key), - ) { array -> - val totalFiatBalance: Lce - val flattenCurrencies: Lce> - if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { - val accountStatusList = array[0] as AccountStatusList - totalFiatBalance = Lce.Content(accountStatusList.totalFiatBalance) - flattenCurrencies = Lce.Content(accountStatusList.flattenCurrencies()) - } else { - val maybeTokenList = array[0] as Lce - totalFiatBalance = maybeTokenList.map { it.totalFiatBalance } - flattenCurrencies = maybeTokenList.map { it.flattenCurrencies() } + ) { 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 } + val flattenCurrencies = lceTokens.map { it.second } + val isReadyToShowRating = array[1] as Boolean + val isNeedToBackup = array[2] as Boolean + val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus + val shouldShowReferralPromo = array[4] as Boolean + val shouldShowSepaBanner = array[5] as Boolean + val shouldShowEnablePushesReminderNotification = array[6] as Boolean + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents) + + addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) + + addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner) + + addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) + + addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) + + addPushReminderNotification( + clickIntents = clickIntents, + shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && + !notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + ) + + addYieldSupplyNotifications(flattenCurrencies) + + val hasCriticalOrWarning = any { notification -> + notification is WalletNotification.Critical || notification is WalletNotification.Warning + } + + if (!hasCriticalOrWarning) { + addRateTheAppNotification(isReadyToShowRating, clickIntents) + } + }.toImmutableList() } - - val isReadyToShowRating = array[1] as Boolean - val isNeedToBackup = array[2] as Boolean - val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus - val shouldShowReferralPromo = array[4] as Boolean - val shouldShowSepaBanner = array[5] as Boolean - val shouldShowEnablePushesReminderNotification = array[6] as Boolean - - buildList { - addUsedOutdatedDataNotification(totalFiatBalance) - - addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) - - addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents) - - addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) - - addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner) - - addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) - - addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) - - addPushReminderNotification( - clickIntents = clickIntents, - shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && - !notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), - ) - - addYieldSupplyNotifications(flattenCurrencies) - - val hasCriticalOrWarning = any { notification -> - notification is WalletNotification.Critical || notification is WalletNotification.Warning - } - - if (!hasCriticalOrWarning) { - addRateTheAppNotification(isReadyToShowRating, clickIntents) - } - }.toImmutableList() - } } private fun MutableList.addUsedOutdatedDataNotification( 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 9565609246..230eac3a61 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 @@ -50,7 +50,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onBuyClick( - portfolioId = portfolioId, + userWalletId = portfolioId.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) @@ -62,7 +62,10 @@ internal class SetCryptoCurrencyActionsTransformer( enabled = true, dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { - clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus = cryptoCurrencyStatus) + clickIntents.onReceiveClick( + portfolioId.userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) }, onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) @@ -87,7 +90,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onSendClick( - portfolioId = portfolioId, + userWalletId = portfolioId.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 18974cedeb..0c3ac24ecd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -9,7 +9,6 @@ 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.card.common.util.getCardsCount -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.exception.RefreshTokenExpiredException @@ -153,7 +152,7 @@ internal class SetVisaInfoTransformer( dimContent = false, onClick = { clickIntents.onReceiveClick( - portfolioId = PortfolioId(userWalletId), // todo account Visa use Main account? + userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, event = MainScreenAnalyticsEvent.ButtonReceive, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt similarity index 50% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt index 9e367377ba..4af0c5bdc7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt @@ -1,33 +1,28 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus.CANCELED -import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED +import com.tangem.domain.pay.model.OrderStatus.UNKNOWN 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.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState import java.util.Currency -internal class TangemPayStateTransformer( +internal class TangemPayInitialStateTransformer( private val value: MainScreenCustomerInfo? = null, - private val onIssueOrderClick: () -> Unit = {}, - private val onContinueKycClick: () -> Unit = {}, + private val onClickIssue: () -> Unit = {}, + private val onClickKyc: () -> Unit = {}, private val openDetails: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> }, - private val issueProgressState: Boolean = false, - private val issueState: Boolean = false, ) : WalletScreenStateTransformer { override fun transform(prevState: WalletScreenState): WalletScreenState { - val tangemPayState = when { - issueProgressState -> createIssueProgressState() - issueState -> createIssueState() - else -> createInitialState() - } + val tangemPayState = createInitialState() return prevState.copy(tangemPayState = tangemPayState) } @@ -35,35 +30,13 @@ internal class TangemPayStateTransformer( val cardInfo = value?.info?.cardInfo return when { value == null -> TangemPayState.Empty - !value.info.isKycApproved() -> createKycInProgressState(onContinueKycClick) + !value.info.isKycApproved -> createKycInProgressState(onClickKyc) cardInfo != null -> getCardInfoState(cardInfo) - value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState() + value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue) else -> createIssueProgressState() } } - private fun createIssueProgressState(): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - buttonText = TextReference.EMPTY, - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = {}, - showProgress = true, - ) - - private fun createIssueState() = Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = onIssueOrderClick, - ) - - private fun createKycInProgressState(onContinueKycClick: () -> Unit): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = onContinueKycClick, - ) - private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card( lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), balanceText = TextReference.Str(getBalanceText(cardInfo)), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt new file mode 100644 index 0000000000..3f66ab33ad --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState + +internal class TangemPayIssueAvailableStateTransformer( + private val onClickIssue: () -> Unit = {}, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState = + prevState.copy(tangemPayState = createIssueAvailableState(onClickIssue)) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt new file mode 100644 index 0000000000..b7bc76806e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState + +internal class TangemPayIssueProgressStateTransformer : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState = + prevState.copy(tangemPayState = createIssueProgressState()) +} \ 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 6a88f70616..72e55cfb23 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 @@ -3,9 +3,9 @@ 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.PortfolioId 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 @@ -18,10 +18,11 @@ import kotlinx.collections.immutable.toImmutableList internal class MultiWalletCurrencyActionsConverter( private val userWallet: UserWallet, - private val portfolioId: PortfolioId, private val clickIntents: WalletCurrencyActionsClickIntents, ) : Converter> { + private val userWalletId: UserWalletId = userWallet.walletId + override fun convert(value: TokenActionsState): ImmutableList { return value.states .filterIfSingleWithToken() @@ -56,17 +57,17 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Buy -> { title = resourceReference(R.string.common_buy) icon = R.drawable.ic_plus_24 - action = { clickIntents.onBuyClick(portfolioId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onBuyClick(userWalletId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Receive -> { title = resourceReference(R.string.common_receive) icon = R.drawable.ic_arrow_down_24 - action = { clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(portfolioId, cryptoCurrencyStatus, actionsState.yield) } + action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.yield) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) @@ -76,7 +77,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Send -> { title = resourceReference(R.string.common_send) icon = R.drawable.ic_arrow_up_24 - action = { clickIntents.onSendClick(portfolioId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onSendClick(userWalletId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) @@ -84,7 +85,7 @@ internal class MultiWalletCurrencyActionsConverter( action = { clickIntents.onSwapClick( cryptoCurrencyStatus = cryptoCurrencyStatus, - portfolioId = portfolioId, + userWalletId = userWalletId, unavailabilityReason = noneReason, ) } @@ -92,12 +93,12 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) icon = R.drawable.ic_copy_24 - action = { clickIntents.onCopyAddressClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onCopyAddressClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.HideToken -> { title = resourceReference(R.string.token_details_hide_token) icon = R.drawable.ic_hide_24 - action = { clickIntents.onHideTokensClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Analytics -> { title = resourceReference(R.string.common_analytics) 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 931921c425..909953e420 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 @@ -41,14 +41,12 @@ internal class TokenListStateConverter( private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId) - clickIntents.onTokenItemClick(id, currencyStatus) + clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) } private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId) - clickIntents.onTokenItemLongClick(id, currencyStatus) + clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) } private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( @@ -112,9 +110,10 @@ internal class TokenListStateConverter( is WalletTokensListState.Empty -> listOf() } return TokensListItemUM.Portfolio( - state = accountItem, + tokenItemUM = accountItem, isExpanded = isExtend, - tokens = items.filterIsInstance(), + isCollapsable = true, + tokens = items.filterIsInstance().toPersistentList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt new file mode 100644 index 0000000000..f8b3293d44 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.state.util + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress + +internal object TangemPayStateCreator { + + fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = onClickKyc, + ) + + fun createIssueAvailableState(onClickIssue: () -> Unit) = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.Res(R.string.common_continue), + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = onClickIssue, + ) + + fun createIssueProgressState(): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.EMPTY, + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = {}, + showProgress = true, + ) +} \ No newline at end of file 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 index b9b464642d..80068f621c 100644 --- 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 @@ -8,7 +8,9 @@ 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.account.AccountStatus +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.RunPolkadotAccountHealthCheckUseCase @@ -63,7 +65,10 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { flow = tokenListFlow(coroutineScope) .onEach { maybeTokenList -> coroutineScope.launch { - sendTokenListAnalytics(maybeTokenList) + sendTokenListAnalytics( + flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(), + totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance, + ) }.saveIn(sendAnalyticsJobHolder) } .distinctUntilChanged() @@ -140,12 +145,14 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine( flow = accountListFlow(coroutineScope) - // todo account analytics for account total balance - /*.onEach { maybeTokenList -> + .onEach { accountStatusList -> coroutineScope.launch { - sendTokenListAnalytics(maybeTokenList) + sendTokenListAnalytics( + flattenCurrencies = accountStatusList.flattenCurrencies(), + totalFiatBalance = accountStatusList.totalFiatBalance, + ) }.saveIn(sendAnalyticsJobHolder) - }*/ + } .distinctUntilChanged() .onEach { accountList -> // todo account see[onAccountListReceived] @@ -207,13 +214,17 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { } } - private suspend fun sendTokenListAnalytics(maybeTokenList: Lce) { + private suspend fun sendTokenListAnalytics( + flattenCurrencies: List?, + totalFiatBalance: TotalFiatBalance?, + ) { val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) tokenListAnalyticsSender.send( displayedUiState = displayedState, userWallet = userWallet, - tokenList = maybeTokenList.getOrNull() ?: return, + flattenCurrencies = flattenCurrencies ?: return, + totalFiatBalance = totalFiatBalance ?: return, ) } 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 140a458bfc..95126a8582 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 @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM @@ -59,6 +60,7 @@ internal fun LazyListScope.portfolioTokensList( contentType = { _, item -> item::class.java }, itemContent = { tokenIndex, token -> val indexWithHeader = tokenIndex.inc() + val lastIndex = tokens.lastIndex.inc() val isPreview = LocalInspectionMode.current val appear = remember { MutableTransitionState(isPreview).apply { targetState = true } @@ -69,11 +71,12 @@ internal fun LazyListScope.portfolioTokensList( .animateItem() .roundedShapeItemDecoration( currentIndex = indexWithHeader, - lastIndex = tokens.lastIndex.inc(), + lastIndex = lastIndex, backgroundColor = TangemTheme.colors.background.primary, ), visibleState = appear, ) { + val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier PortfolioTokensListItem( state = token, isBalanceHidden = isBalanceHidden, @@ -114,10 +117,15 @@ private fun LazyListScope.portfolioItem( modifier = anchorModifier, visibleState = appear, ) { + val modifier = if (portfolio.tokens.isEmpty()) { + Modifier.padding(vertical = 8.dp) + } else { + Modifier.padding(top = 8.dp) + } PortfolioListItem( state = portfolio, isBalanceHidden = isBalanceHidden, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = modifier, ) } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt deleted file mode 100644 index 8ab07acad0..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.utils - -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase -import com.tangem.domain.tokens.RemoveCurrencyUseCase -import javax.inject.Inject - -class WalletFeatureUseCasesFacade @Inject constructor( - private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, -) { - - suspend fun isCryptoCurrencyCoinCouldHide(portfolioId: PortfolioId, cryptoCurrencyCoin: CryptoCurrency.Coin) = - when (portfolioId) { - is PortfolioId.Account -> TODO("account") - is PortfolioId.Wallet -> isCryptoCurrencyCoinCouldHide(portfolioId.userWalletId, cryptoCurrencyCoin) - } - - suspend fun removeCurrencyUseCase(portfolioId: PortfolioId, currency: CryptoCurrency) = when (portfolioId) { - is PortfolioId.Account -> TODO("account") - is PortfolioId.Wallet -> removeCurrencyUseCase(portfolioId.userWalletId, currency) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 0bf16b6415..1d3a97b3a6 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -16,13 +16,16 @@ dependencies { implementation(projects.features.walletconnect.api) implementation(projects.features.sendV2.api) - /** Core */ - implementation(projects.core.configToggles) - implementation(projects.core.decompose) - implementation(projects.core.ui) + /** Common */ implementation(projects.common.routing) implementation(projects.common.ui) + + /** Core */ implementation(projects.core.analytics) + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.ui) /** Domain models */ implementation(projects.domain.appCurrency.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index f079b379c0..8afd70342c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -110,7 +110,9 @@ private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Mo Image( painter = painterResource(R.drawable.img_wallet_connect_76), contentDescription = "Wallet Connect", - modifier = Modifier.size(76.dp), + modifier = Modifier + .size(76.dp) + .testTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE), ) Text( modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index 0bdad0e725..57aef929c4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -11,4 +11,5 @@ internal data class WcSpendAllowanceUM( val tokenSymbol: String, val tokenImageUrl: String?, val networkIconRes: Int?, + val onLearnMoreClicked: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file 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 8d36bbe2c6..8b201362c2 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 @@ -16,6 +16,7 @@ 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.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2.Icon.Type @@ -55,6 +56,7 @@ import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransacti import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiveBlockAidUiConverter import com.tangem.features.walletconnect.utils.WcNotificationsFactory +import com.tangem.utils.SupportedLanguages import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -74,11 +76,11 @@ internal class WcSendTransactionModel @Inject constructor( private val clipboardManager: ClipboardManager, private val useCaseFactory: WcRequestUseCaseFactory, private val converter: WcSendTransactionUMConverter, - private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, private val getFeeUseCase: GetFeeUseCase, private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, private val notificationsFactory: WcNotificationsFactory, private val analytics: AnalyticsEventHandler, + private val urlOpener: UrlOpener, ) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -94,6 +96,7 @@ internal class WcSendTransactionModel @Inject constructor( private var signState: WcSignState<*> by Delegates.notNull() private var wcApproval: WcApproval? = null private var sign: () -> Unit = {} + private val blockAidUiConverter = WcSendAndReceiveBlockAidUiConverter() private val feeReloadState = MutableStateFlow(false) private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) private val securityStatusState = @@ -243,8 +246,9 @@ internal class WcSendTransactionModel @Inject constructor( val blockAidState = when (securityCheck) { is Lce.Content -> blockAidUiConverter.convert( WcSendAndReceiveBlockAidUiConverter.Input( - securityCheck.content.result, - if (isApproval) wcApproval?.getAmount() else null, + result = securityCheck.content.result, + approvedAmount = if (isApproval) wcApproval?.getAmount() else null, + onApproveLearnMoreClick = ::onApproveLearnMoreClick, ), ) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) @@ -291,6 +295,15 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pop() } + private fun onApproveLearnMoreClick() { + val code = SupportedLanguages.getCurrentSupportedLanguageCode() + .takeIf { it == SupportedLanguages.RUSSIAN } + ?: SupportedLanguages.ENGLISH + + val url = "https://tangem.com/$code/blog/post/give-revoke-permission/" + urlOpener.openUrl(url) + } + private fun isMultipleSignRequired(useCase: WcSignUseCase<*>): Boolean { return if (useCase is SignRequirements) { useCase.isMultipleSignRequired() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt index 60c4fc70c2..a4e431c1a1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt @@ -255,6 +255,7 @@ private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvide amountValue = BigDecimal("100"), tokenSymbol = "ETH", isUnlimited = false, + onLearnMoreClicked = {}, ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt index 2b7aca4340..92dbcdd661 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -3,25 +3,29 @@ package com.tangem.features.walletconnect.transaction.ui.blockaid 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.Modifier +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Devices 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.extensions.TextReference -import com.tangem.core.ui.extensions.isNullOrEmpty -import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowanceItem import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Composable internal fun TransactionCheckResultsItem( @@ -29,11 +33,7 @@ internal fun TransactionCheckResultsItem( onClickAllowToSpend: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { if (item.isLoading) { WcEstimatedWalletChangesLoadingItem() } else { @@ -44,6 +44,10 @@ internal fun TransactionCheckResultsItem( WcEstimatedWalletChangesItem(item.estimatedWalletChanges) } else if (item.spendAllowance != null) { WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend) + ApproveDescription( + modifier = Modifier.padding(bottom = 6.dp, start = 12.dp, end = 12.dp), + onLearnMoreClick = item.spendAllowance.onLearnMoreClicked, + ) } else if (!item.additionalNotification.isNullOrEmpty()) { WcEstimatedWalletChangesNotificationItem(description = item.additionalNotification) } else { @@ -53,6 +57,29 @@ internal fun TransactionCheckResultsItem( } } +@Composable +private fun ApproveDescription(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) { + val linkText = stringResourceSafe(R.string.common_learn_more) + val fullString = stringResourceSafe(R.string.wc_approve_description) + val defaultColor = TangemTheme.colors.text.tertiary + val linkColor = TangemTheme.colors.text.accent + Text( + modifier = modifier, + style = TangemTheme.typography.caption2, + text = buildAnnotatedString { + appendColored(fullString, defaultColor) + appendSpace() + withLink( + link = LinkAnnotation.Clickable( + tag = "WC_APPROVE_LEARN_MORE_TAG", + linkInteractionListener = { onLearnMoreClick() }, + ), + block = { appendColored(text = linkText, color = linkColor) }, + ) + }, + ) +} + @Composable @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -95,5 +122,22 @@ private class TransactionCheckResultsItemProvider : PreviewParameterProvider { override fun convert(value: Input): WcEstimatedWalletChangeUM { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 3ef6afbf4c..e8777d39f3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -20,15 +20,16 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal -import javax.inject.Inject private const val DECIMALS_AMOUNT = 2 @Suppress("CyclomaticComplexMethod", "LongMethod") -internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( - private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, - private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, -) : Converter { +internal class WcSendAndReceiveBlockAidUiConverter : + Converter { + + private val estimatedWalletChangeUMConverter = WcEstimatedWalletChangeUMConverter() + private val spendAllowanceUMConverter = WcSpendAllowanceUMConverter() + override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM { val description = value.result.description?.let { if (it.isNotEmpty()) TextReference.Str(it) else null } val simulation = value.result.simulation @@ -118,7 +119,12 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( when (data) { is SimulationData.SendAndReceive, SimulationData.NoWalletChangesDetected -> null is SimulationData.Approve -> value.approvedAmount?.let { - spendAllowanceUMConverter.convert(it) + spendAllowanceUMConverter.convert( + WcSpendAllowanceUMConverter.Input( + approvedAmount = it, + onLearnMoreClick = value.onApproveLearnMoreClick, + ), + ) } } }, @@ -128,6 +134,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( data class Input( val result: CheckTransactionResult, val approvedAmount: WcApprovedAmount?, + val onApproveLearnMoreClick: () -> Unit, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt index 4f7bf34268..e79abdf12d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -6,24 +6,26 @@ import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.utils.converter.Converter -import javax.inject.Inject -internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { +internal class WcSpendAllowanceUMConverter : Converter { - override fun convert(value: WcApprovedAmount): WcSpendAllowanceUM { - val amount = value.amount?.value ?: 0.0.toBigDecimal() - val isUnlimited = value.amount?.value == null + override fun convert(value: Input): WcSpendAllowanceUM { + val amount = value.approvedAmount.amount?.value ?: 0.0.toBigDecimal() + val isUnlimited = value.approvedAmount.amount?.value == null return WcSpendAllowanceUM( amountValue = amount, - amountText = if (value.amount?.value == null) { + amountText = if (value.approvedAmount.amount?.value == null) { TextReference.Res(R.string.wc_common_unlimited) } else { TextReference.Str(amount.amountText()) }, isUnlimited = isUnlimited, - tokenSymbol = value.amount?.currencySymbol ?: "", - tokenImageUrl = value.logoUrl, - networkIconRes = value.chainId?.toString()?.let { getActiveIconRes(it) }, + tokenSymbol = value.approvedAmount.amount?.currencySymbol ?: "", + tokenImageUrl = value.approvedAmount.logoUrl, + networkIconRes = value.approvedAmount.chainId?.toString()?.let { getActiveIconRes(it) }, + onLearnMoreClicked = value.onLearnMoreClick, ) } + + data class Input(val approvedAmount: WcApprovedAmount, val onLearnMoreClick: () -> Unit) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index a1d574e005..1f02216d53 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM @@ -51,6 +52,7 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Suppress("LongParameterList", "LongMethod") @Composable @@ -316,5 +318,34 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide ), transactionValidationResult = ValidationResult.SAFE, ), + WcSendTransactionItemUM( + onDismiss = {}, + onSend = {}, + appInfo = WcTransactionAppInfoContentUM( + appName = "React App", + appIcon = "", + verifiedState = VerifiedDAppState.Verified {}, + appSubtitle = "react-app.walletconnect.com", + ), + estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM( + isLoading = false, + spendAllowance = WcSpendAllowanceUM( + amountValue = BigDecimal.ZERO, + isUnlimited = false, + amountText = stringReference("0.00 WPOL"), + tokenSymbol = "", + tokenImageUrl = "", + networkIconRes = 0, + onLearnMoreClicked = {}, + ), + ), + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + feeState = WcTransactionFeeState.None, + address = "0xdac17f958d2ee523a2206206994597c13d831ec7", + sendEnabled = true, + feeErrorNotification = null, + transactionValidationResult = ValidationResult.SAFE, + ), ), ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt index 14baa04a9a..d00eede2f9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal @Immutable internal sealed class YieldSupplyFeeUM { @@ -27,4 +28,5 @@ internal data class YieldSupplyActionUM( val yieldSupplyFeeUM: YieldSupplyFeeUM, val isPrimaryButtonEnabled: Boolean, val isTransactionSending: Boolean, + val maxFee: BigDecimal = BigDecimal.ZERO, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt new file mode 100644 index 0000000000..9480ba16ad --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt @@ -0,0 +1,6 @@ +package com.tangem.features.yield.supply.impl.main.entity + +internal enum class LoadingStatusMode { + Initial, + LoadApy, +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt index de7a778530..192ea9551a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt @@ -6,7 +6,9 @@ import com.tangem.core.ui.extensions.TextReference @Immutable internal sealed class YieldSupplyUM { - data class Initial( + data object Initial : YieldSupplyUM() + + data class Available( val title: TextReference, val onClick: () -> Unit, ) : YieldSupplyUM() diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index d4c2c34325..ad5f40b8cf 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -8,8 +8,6 @@ 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.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -18,10 +16,15 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase import com.tangem.features.yield.supply.api.YieldSupplyComponent -import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusFailureTransformer +import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.DelayedWork import kotlinx.coroutines.CoroutineScope @@ -29,6 +32,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import com.tangem.utils.transformer.update import javax.inject.Inject import kotlin.properties.Delegates @@ -44,12 +48,15 @@ internal class YieldSupplyModel @Inject constructor( @DelayedWork private val coroutineScope: CoroutineScope, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, + private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, + private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, + private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() val uiState: StateFlow - field = MutableStateFlow(YieldSupplyUM.Loading) + field = MutableStateFlow(YieldSupplyUM.Initial) val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -68,8 +75,17 @@ internal class YieldSupplyModel @Inject constructor( field = MutableStateFlow(false) init { - subscribeOnCurrencyStatusUpdates() - subscribeOnBalanceHidden() + checkIfYieldSupplyIsAvailable() + } + + private fun checkIfYieldSupplyIsAvailable() { + modelScope.launch(dispatchers.io) { + val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency) + if (isAvailable) { + subscribeOnCurrencyStatusUpdates() + subscribeOnBalanceHidden() + } + } } private fun subscribeOnCurrencyStatusUpdates() { @@ -102,24 +118,20 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun loadTokenStatus(cryptoCurrency: CryptoCurrency.Token) { + private fun loadTokenStatus(mode: LoadingStatusMode) { + val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrency) + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) .onRight { tokenStatus -> - val newState = if (tokenStatus.isActive) { - YieldSupplyUM.Initial( - title = resourceReference( - id = R.string.yield_module_token_details_earn_notification_title, - formatArgs = wrappedList(tokenStatus.apy), - ), - onClick = ::onStartEarningClick, - ) - } else { - YieldSupplyUM.Unavailable - } - uiState.update { newState } + uiState.update( + YieldSupplyTokenStatusSuccessTransformer( + tokenStatus = tokenStatus, + onStartEarningClick = ::onStartEarningClick, + mode = mode, + ), + ) }.onLeft { - uiState.update { YieldSupplyUM.Unavailable } + uiState.update(YieldSupplyTokenStatusFailureTransformer(mode)) } } } @@ -154,6 +166,7 @@ internal class YieldSupplyModel @Inject constructor( val yieldTransaction = cryptoCurrencyStatus.value.pendingTransactions.firstOrNull { it.type is TxInfo.TransactionType.YieldSupply }?.type as? TxInfo.TransactionType.YieldSupply + sendInfoAboutProtocolStatus(yieldSupplyStatus?.isActive == true) val yieldSupplyUM = when { hasActiveTransaction && yieldTransaction != null -> { @@ -178,8 +191,21 @@ internal class YieldSupplyModel @Inject constructor( uiState.update { yieldSupplyUM } - if (yieldSupplyUM is YieldSupplyUM.Loading) { - (cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus) + when (yieldSupplyUM) { + is YieldSupplyUM.Loading -> loadTokenStatus(LoadingStatusMode.Initial) + is YieldSupplyUM.Content -> loadTokenStatus(LoadingStatusMode.LoadApy) + else -> Unit + } + } + + private fun sendInfoAboutProtocolStatus(isActivated: Boolean) { + val token = cryptoCurrency as? CryptoCurrency.Token ?: return + modelScope.launch(dispatchers.default) { + if (isActivated) { + yieldSupplyActivateUseCase(token) + } else { + yieldSupplyDeactivateUseCase(token) + } } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt new file mode 100644 index 0000000000..0154fa52ef --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.yield.supply.impl.main.model.transformers + +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.utils.transformer.Transformer + +internal class YieldSupplyTokenStatusFailureTransformer( + private val mode: LoadingStatusMode, +) : Transformer { + + override fun transform(prevState: YieldSupplyUM): YieldSupplyUM { + return when (mode) { + LoadingStatusMode.Initial -> YieldSupplyUM.Unavailable + LoadingStatusMode.LoadApy -> prevState // TODO apply correct UI + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt new file mode 100644 index 0000000000..f45986dc3c --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.features.yield.supply.impl.main.model.transformers + +import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus +import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.utils.transformer.Transformer + +internal class YieldSupplyTokenStatusSuccessTransformer( + private val tokenStatus: YieldMarketTokenStatus, + private val onStartEarningClick: () -> Unit, + private val mode: LoadingStatusMode, +) : Transformer { + + override fun transform(prevState: YieldSupplyUM): YieldSupplyUM { + if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable + + return when (mode) { + LoadingStatusMode.Initial -> { + YieldSupplyUM.Available( + title = resourceReference( + id = R.string.yield_module_token_details_earn_notification_title, + formatArgs = wrappedList(tokenStatus.apy), + ), + onClick = onStartEarningClick, + ) + } + LoadingStatusMode.LoadApy -> { + if (prevState is YieldSupplyUM.Content) { + prevState.copy( + rewardsApy = stringReference("${tokenStatus.apy}%"), + ) + } else { + prevState + } + } + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt index df41b8fb28..e044b1c598 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt @@ -44,7 +44,7 @@ internal fun YieldSupplyBlockContent( modifier = modifier, ) { supplyUM -> when (supplyUM) { - is YieldSupplyUM.Initial -> SupplyInitial(supplyUM) + is YieldSupplyUM.Available -> SupplyAvailable(supplyUM) YieldSupplyUM.Loading -> SupplyLoading() is YieldSupplyUM.Content -> SupplyContent(supplyUM, isBalanceHidden) YieldSupplyUM.Processing.Enter -> SupplyProcessing( @@ -54,12 +54,13 @@ internal fun YieldSupplyBlockContent( resourceReference(R.string.yield_module_stop_earning), ) YieldSupplyUM.Unavailable -> SupplyUnavailable() + YieldSupplyUM.Initial -> {} } } } @Composable -private fun SupplyInitial(supplyUM: YieldSupplyUM.Initial) { +private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available) { SupplyInfo( title = supplyUM.title, subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description), @@ -292,7 +293,7 @@ private fun YieldSupplyBlockContent_Preview(@PreviewParameter(PreviewProvider::c private class PreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - YieldSupplyUM.Initial( + YieldSupplyUM.Available( title = TextReference.Res( R.string.yield_module_token_details_earn_notification_title, wrappedList("5.1"), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt index 4d7abbb513..db0f137890 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt @@ -8,16 +8,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp 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.SecondaryButton import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.core.ui.extensions.stringResourceSafe +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.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveModel import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveContent import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveTitle import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent import kotlinx.coroutines.flow.StateFlow internal class YieldSupplyActiveComponent( @@ -26,6 +29,12 @@ internal class YieldSupplyActiveComponent( ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { private val model: YieldSupplyActiveModel = getOrCreateModel(params = params) + private val chartComponent = DefaultYieldSupplyChartComponent( + appComponentContext = child("chartComponent"), + params = DefaultYieldSupplyChartComponent.Params( + cryptoCurrency = params.cryptoCurrencyStatusFlow.value.currency as CryptoCurrency.Token, + ), + ) @Composable override fun Title() { @@ -37,7 +46,12 @@ internal class YieldSupplyActiveComponent( val state by model.uiState.collectAsStateWithLifecycle() val isBalanceHidden by params.isBalanceHiddenFlow.collectAsStateWithLifecycle() - YieldSupplyActiveContent(state = state, isBalanceHidden = isBalanceHidden, modifier = Modifier) + YieldSupplyActiveContent( + state = state, + isBalanceHidden = isBalanceHidden, + chartComponent = chartComponent, + modifier = Modifier, + ) } @Composable diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt index 7cda12cb63..bf0fdc6e5b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt @@ -10,4 +10,5 @@ internal data class YieldSupplyActiveContentUM( val subtitle: TextReference, val subtitleLink: TextReference, val notificationUM: NotificationUM?, + val apy: TextReference? = null, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt index 4b38fbb125..ae85f3f637 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt @@ -5,18 +5,22 @@ 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.notifications.NotificationConfig +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.format +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveComponent import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -24,6 +28,7 @@ internal class YieldSupplyActiveModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase, + private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, ) : Model() { private val params: YieldSupplyActiveComponent.Params = paramsContainer.require() @@ -87,6 +92,8 @@ internal class YieldSupplyActiveModel @Inject constructor( null } + loadApy() + uiState.update { it.copy( notificationUM = approvalNotification, @@ -104,6 +111,21 @@ internal class YieldSupplyActiveModel @Inject constructor( .launchIn(modelScope) } + private fun loadApy() { + val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return + modelScope.launch(dispatchers.default) { + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken).onRight { tokenStatus -> + uiState.update { + it.copy( + apy = TextReference.Str("${tokenStatus.apy}%"), + ) + } + }.onLeft { + Timber.e("Error loading token status") + } + } + } + private companion object { const val AAVEV3_PREFIX = "a" } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt index 0619e6b7a0..383a4b2231 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -15,6 +16,7 @@ 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.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.withLink @@ -25,6 +27,7 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -35,6 +38,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSu internal fun YieldSupplyActiveContent( state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean, + chartComponent: ComposableContentComponent, modifier: Modifier = Modifier, ) { Column( @@ -62,6 +66,9 @@ internal fun YieldSupplyActiveContent( style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) + + CurrentApy(state.apy) + chartComponent.Content(Modifier.padding(bottom = 12.dp)) } YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden) @@ -76,6 +83,45 @@ internal fun YieldSupplyActiveContent( } } +@Composable +private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { + Row(modifier = modifier.padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = modifier.weight(1.0f), + text = stringResourceSafe(R.string.yield_module_earn_sheet_current_apy_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + AnimatedContent( + targetState = apy?.resolveReference(), + label = "CurrentApy", + ) { apyText -> + if (apyText == null) { + TextShimmer( + modifier = modifier.width(56.dp), + text = "", + style = TangemTheme.typography.body1, + ) + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(R.drawable.ic_arrow_up_8), + tint = TangemTheme.colors.text.accent, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + ) + Text( + modifier = modifier, + text = apyText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + } + } + } +} + @Composable private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean) { Column( @@ -193,7 +239,11 @@ private fun YieldSupplyActiveBottomSheet_Preview( @PreviewParameter(YieldSupplyActiveBottomSheetPreviewProvider::class) params: YieldSupplyActiveContentUM, ) { TangemThemePreview { - YieldSupplyActiveContent(params, true) + YieldSupplyActiveContent( + state = params, + isBalanceHidden = true, + chartComponent = ComposableContentComponent.EMPTY, + ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index a82ff670c8..991aa736fa 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -8,8 +8,10 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.FetchCurrencyStatusUseCase @@ -17,7 +19,9 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory @@ -55,6 +59,8 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, + private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, + private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() @@ -106,13 +112,26 @@ internal class YieldSupplyStartEarningModel @Inject constructor( } } + private suspend fun getMaxFee(): BigDecimal? { + if (uiState.value.maxFee != BigDecimal.ZERO) return uiState.value.maxFee + val yieldTokenStatus = yieldSupplyGetTokenStatusUseCase(cryptoCurrency as CryptoCurrency.Token) + .getOrNull() + return yieldTokenStatus?.maxFeeNative?.parseToBigDecimal(cryptoCurrency.decimals) + } + private suspend fun onLoadFee() { if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading || uiState.value.isTransactionSending) return + val maxFee = if (uiState.value.maxFee == BigDecimal.ZERO) { + getMaxFee() + } else { + uiState.value.maxFee + } ?: return + val transactionListData = yieldSupplyStartEarningUseCase( userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - maxNetworkFee = MAX_NETWORK_FEE, + maxNetworkFee = maxFee, ).getOrNull() ?: return uiState.update { @@ -146,7 +165,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( appCurrency = appCurrency, updatedTransactionList = updatedTransactionList, feeValue = feeSum, - maxNetworkFee = MAX_NETWORK_FEE, + maxNetworkFee = maxFee, ), ) yieldSupplyNotificationsUpdateTrigger.triggerUpdate( @@ -189,6 +208,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( }, ifRight = { fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id) + yieldSupplyActivateUseCase(cryptoCurrency as CryptoCurrency.Token) modelScope.launch { params.callback.onTransactionSent() } @@ -274,8 +294,4 @@ internal class YieldSupplyStartEarningModel @Inject constructor( popBack = params.callback::onBackClick, ) } - - private companion object { - val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO replace with value from api - } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index b1bba83d10..4204a8bc55 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -15,6 +15,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory @@ -49,6 +50,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( private val urlOpener: UrlOpener, private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, + private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() @@ -134,6 +136,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ) }, ifRight = { + yieldSupplyDeactivateUseCase(cryptoCurrency as CryptoCurrency.Token) params.callback.onTransactionSent() }, ) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 34e16dd458..35e4b6fd2f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -96,7 +96,7 @@ room = "2.6.1" markdown = "0.7.2" markdownComposeView = "0.5.4" usedesk = "4.4.0" -sumsub = "1.37.1" +sumsub = "1.38.0" # endregion Other libraries # region Tools diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index d1d5a501c6..381452eefa 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -169,6 +169,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "hyperevm/test" -> Blockchain.HyperliquidTestnet "quai-network" -> Blockchain.Quai "quai-network/test" -> Blockchain.QuaiTestnet + "linea" -> Blockchain.Linea + "linea/test" -> Blockchain.LineaTestnet else -> null } } @@ -335,6 +337,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.HyperliquidTestnet -> "hyperevm/test" Blockchain.Quai -> "quai-network" Blockchain.QuaiTestnet -> "quai-network/test" + Blockchain.Linea -> "linea" + Blockchain.LineaTestnet -> "linea/test" } } @@ -440,6 +444,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> "hyperliquid" Blockchain.Quai, Blockchain.QuaiTestnet -> "quai-network" + Blockchain.Linea, Blockchain.LineaTestnet -> "linea" } } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 6008e6ea58..1312cb0581 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -3,6 +3,7 @@ package com.tangem.lib.crypto.derivation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.isUTXO import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.network.Network /** * Utility class to recognize the account node in a derivation path based on the blockchain type. @@ -21,12 +22,21 @@ class AccountNodeRecognizer(blockchain: Blockchain) { NON_UTXO_BLOCKCHAIN_NODE_INDEX } + /** Recognizes the account node value from the given [derivationPath] */ + fun recognize(derivationPath: Network.DerivationPath): Long? { + val derivationPathValue = derivationPath.value ?: return null + + return recognize(derivationPathValue = derivationPathValue) + } + /** Recognizes the account node value from the given derivation path string [derivationPathValue] */ fun recognize(derivationPathValue: String): Long? { + if (derivationPathValue.isBlank()) return null + return runCatching { - recognize(derivationPath = DerivationPath(rawPath = derivationPathValue)) - } - .getOrNull() + val cardSdkDerivationPath = DerivationPath(rawPath = derivationPathValue) + recognize(derivationPath = cardSdkDerivationPath) + }.getOrNull() } /** Recognizes the account node value from the given [derivationPath] */