From 2071dd9e2d34d149c060fd9c336deb17cd63a634 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 May 2026 23:29:32 +0400 Subject: [PATCH 01/16] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 48 ++++++++++++++++++- .../tangem/feature/swap/model/SwapModel.kt | 26 +++++++++- .../feature/swap/models/SwapStateHolder.kt | 1 + .../tangem/feature/swap/models/UiActions.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 1 + .../com/tangem/feature/swap/ui/SwapScreen.kt | 5 +- 6 files changed, 77 insertions(+), 5 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 9a5ad696ea..c17789bc7a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -8,12 +8,15 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER +import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.core.analytics.models.getReferralParams import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.FeeBucket private const val SWAP_CATEGORY = "Swap" @@ -37,6 +40,39 @@ sealed class SwapEvents( ), ), AppsFlyerIncludedEvent + class SwapType(val mode: SwapUIMode) : SwapEvents( + event = "Swap type simple/detailed", + params = mapOf("Swap type" to mode.key), + ) + + class SwapTypeSelect( + val provider: SwapProvider?, + val sendToken: String, + val sendBlockchain: String, + val receiveToken: String?, + val receiveBlockchain: String?, + ) : SwapEvents( + event = "Button - Swap type menu", + params = buildMap { + provider?.let { put(PROVIDER, it.name) } + put(SEND_TOKEN, sendToken) + put(SEND_BLOCKCHAIN, sendBlockchain) + receiveToken?.let { put(RECEIVE_TOKEN, it) } + receiveBlockchain?.let { put(RECEIVE_BLOCKCHAIN, it) } + }, + ) + + class SwapTypeReSelection( + val typeFrom: SwapUIMode, + val typeTo: SwapUIMode, + ) : SwapEvents( + event = "Swap type re-selection", + params = mapOf( + "Type from" to typeFrom.key, + "Type to" to typeTo.key, + ), + ) + class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") class ChooseTokenScreenResult( @@ -75,9 +111,17 @@ sealed class SwapEvents( ), ) - class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + class ButtonSwapClicked( + val sendToken: String, + val receiveToken: String, + val swapUIMode: SwapUIMode, + ) : SwapEvents( event = "Button - Swap", - params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Swap type" to swapUIMode.key, + ), ) class ButtonGivePermissionClicked( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index cd731174ee..e5cf890a31 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -302,7 +302,9 @@ internal class SwapModel @Inject constructor( }.launchIn(modelScope) modelScope.launch { - uiState = uiState.copy(swapUIMode = getSwapUiModeUseCase()) + val swapUIMode = getSwapUiModeUseCase() + uiState = uiState.copy(swapUIMode = swapUIMode) + analyticsEventHandler.send(SwapEvents.SwapType(swapUIMode)) } } @@ -1662,6 +1664,7 @@ internal class SwapModel @Inject constructor( SwapEvents.ButtonSwapClicked( sendToken = sendTokenSymbol, receiveToken = receiveTokenSymbol, + swapUIMode = uiState.swapUIMode, ), ) } @@ -1765,15 +1768,34 @@ internal class SwapModel @Inject constructor( router.replaceAll(SwapRoute.Success) }, onSwapUIModeChange = ::onSwapUIModeChange, + onSwapTypeMenuOpened = ::onSwapTypeMenuOpened, ) } private fun onSwapUIModeChange(mode: SwapUIMode) { - if (uiState.swapUIMode == mode) return + val currentMode = uiState.swapUIMode + if (currentMode == mode) return + analyticsEventHandler.send( + SwapEvents.SwapTypeReSelection(typeFrom = currentMode, typeTo = mode), + ) uiState = uiState.copy(swapUIMode = mode) modelScope.launch { setSwapUiModeUseCase(mode) } } + private fun onSwapTypeMenuOpened() { + val fromCurrency = dataState.fromSwapCurrencyStatus?.currency + val toCurrency = dataState.toSwapCurrencyStatus?.currency + analyticsEventHandler.send( + SwapEvents.SwapTypeSelect( + provider = dataState.selectedProvider, + sendToken = fromCurrency?.symbol.orEmpty(), + sendBlockchain = fromCurrency?.network?.name.orEmpty(), + receiveToken = toCurrency?.symbol, + receiveBlockchain = toCurrency?.network?.name, + ), + ) + } + private fun selectWalletInSelector( fromSwapCurrencyStatus: SwapCurrencyStatus?, toSwapCurrencyStatus: SwapCurrencyStatus?, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 330e5f5188..ff8dfd3624 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -46,6 +46,7 @@ internal data class SwapStateHolder( val onPredefinedPercentSelected: ((PredefinedPercentAmount) -> Unit)? = null, val onShowPermissionBottomSheet: () -> Unit = {}, val onSwapUIModeChange: (SwapUIMode) -> Unit = {}, + val onSwapTypeMenuOpened: () -> Unit = {}, ) @Immutable diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index cc76ec32dd..f9714cdded 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -30,4 +30,5 @@ internal data class UiActions( val onLinkClick: (String) -> Unit, val onReceiveCardWarningClick: () -> Unit, val onSwapUIModeChange: (SwapUIMode) -> Unit, + val onSwapTypeMenuOpened: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7380667baa..7236b2fa02 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -108,6 +108,7 @@ internal class StateBuilder( isInsufficientFunds = false, swapUIMode = swapUIMode, onSwapUIModeChange = actions.onSwapUIModeChange, + onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened, shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled, isPredefinedButtonsEnabled = swapFeatureToggles.isSwapPredefinedButtonsEnabled, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 337dbe062d..0327594f81 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -87,7 +87,10 @@ private fun SwapTopBar(stateHolder: SwapStateHolder) { backIconRes = R.drawable.ic_close_24, iconRes = if (stateHolder.shouldShowAbMenu) R.drawable.ic_more_vertical_24 else null, onIconClick = if (stateHolder.shouldShowAbMenu) { - { shouldShowModeMenu = true } + { + stateHolder.onSwapTypeMenuOpened() + shouldShowModeMenu = true + } } else { null }, From f8736fa64b3d33788dea4d9be9c996bc531f4253 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 May 2026 14:09:23 +0500 Subject: [PATCH 02/16] Updated on 2026-08-14 --- .../scenarios/CheckMainScreenScenarios.kt | 12 ++-- .../tangem/screens/ChooseTokenPageObject.kt | 39 +++++++++++ .../screens/GetTokenBottomSheetPageObject.kt | 45 +++++++++++++ .../tangem/screens/MainScreenPageObject.kt | 5 ++ .../TangemPayAddFundsSheetPageObject.kt | 2 +- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 66 ++++++++++++------- .../MainScreenActionButtonsTest.kt | 48 +++++++------- .../com/tangem/tests/main/HideTokenTest.kt | 5 +- .../com/tangem/tests/main/MainScreenTest.kt | 15 ++++- 9 files changed, 180 insertions(+), 57 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ChooseTokenPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/GetTokenBottomSheetPageObject.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt index b8baf03580..75e7af6d4d 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt @@ -97,8 +97,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen( step("Assert devices count equal to '$devicesCount'") { onMainScreen { walletDevicesCount.assertTextContains(devicesCount) } } - step("Assert 'Buy' button is displayed") { - onMainScreen { buyButton.assertIsDisplayed() } + step("Assert 'Add funds' button is displayed") { + onMainScreen { addFundsButton.assertIsDisplayed() } } step("Assert 'Swap' button is displayed") { onMainScreen { swapButton.assertIsDisplayed() } @@ -119,8 +119,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen( fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) { if (isEnabled) { - step("Assert 'Buy' button is enabled") { - onMainScreen { buyButton.assertIsEnabled() } + step("Assert 'Add funds' button is enabled") { + onMainScreen { addFundsButton.assertIsEnabled() } } step("Assert 'Swap' button is enabled") { onMainScreen { swapButton.assertIsEnabled() } @@ -129,8 +129,8 @@ fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = onMainScreen { sellButton.assertIsEnabled() } } } else { - step("Assert 'Buy' button is not enabled") { - onMainScreen { buyButton.assertIsNotEnabled() } + step("Assert 'Add funds' button is not enabled") { + onMainScreen { addFundsButton.assertIsNotEnabled() } } step("Assert 'Swap' button is not enabled") { onMainScreen { swapButton.assertIsNotEnabled() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ChooseTokenPageObject.kt new file mode 100644 index 0000000000..28ebe26a4a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ChooseTokenPageObject.kt @@ -0,0 +1,39 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseSearchBarTestTags +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.test.TokenElementsTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText + +/** + * "You receive" token chooser opened from the main-screen "Add funds" button. + */ +class ChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val searchBar: KNode = child { + hasTestTag(BaseSearchBarTestTags.SEARCH_BAR) + } + + fun tokenWithTitle(tokenTitle: String): KNode = child { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasAnyDescendant(withTestTag(TokenElementsTestTags.TOKEN_TITLE)) + hasAnyDescendant(withText(tokenTitle)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onChooseTokenScreen(function: ChooseTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/GetTokenBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/GetTokenBottomSheetPageObject.kt new file mode 100644 index 0000000000..508f0a1676 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/GetTokenBottomSheetPageObject.kt @@ -0,0 +1,45 @@ +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.BaseBottomSheetTestTags +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 + +/** + * "Get token" bottom sheet shown after picking a token in the Add funds flow. + * Contains quick actions (Buy / Receive / …) and the "Go to token" button. + */ +class GetTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(BaseBottomSheetTestTags.TITLE) + } + + val closeButton: KNode = child { + hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON) + } + + // The "Get token" sheet action rows use combinedClickable; the row's testTag lands on a + // separate zero-bounds semantics node that fails assertIsDisplayed. Matching the merged node + // by its title text yields the displayed, clickable row (performClick injects a touch at its + // center, which the row's clickable handles). + val buyButton: KNode = child { + hasText(getResourceString(R.string.common_buy)) + } + + val receiveButton: KNode = child { + hasText(getResourceString(R.string.common_receive)) + } + + val goToTokenButton: KNode = child { + hasText(getResourceString(R.string.common_go_to_token)) + } +} + +internal fun BaseTestCase.onGetTokenBottomSheet(function: GetTokenBottomSheetPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 82defdf7f0..db62a6786a 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -52,6 +52,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_buy)) } + val addFundsButton: KNode = child { + hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_add_funds)) + } + val sendButton: KNode = child { hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasText(getResourceString(R.string.common_send)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt index 599c234915..ba66970bb8 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt @@ -12,7 +12,7 @@ class TangemPayAddFundsSheetPageObject(semanticsProvider: SemanticsNodeInteracti ComposeScreen(semanticsProvider = semanticsProvider) { val swapOption: KNode = child { - hasText(getResourceString(CoreResR.string.common_exchange)) + hasText(getResourceString(CoreResR.string.tangempay_topup_swap_title)) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 3c44cb4154..becda76ceb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -40,15 +40,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Assert error notification title is displayed") { onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() } } @@ -84,15 +87,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Click on 'Confirm' button in 'Dialog'") { onDialog { confirmButton.clickWithAssertion() } } @@ -155,15 +161,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Click on 'Confirm' button in 'Dialog'") { onDialog { confirmButton.clickWithAssertion() } } @@ -238,15 +247,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Click on 'Confirm' button in 'Dialog'") { onDialog { confirmButton.clickWithAssertion() } } @@ -320,15 +332,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Click on 'Confirm' button in 'Dialog'") { onDialog { confirmButton.clickWithAssertion() } } @@ -406,15 +421,18 @@ class BuyTokenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.clickWithAssertion() } } step("Click on token with name: '$tokenTitle'") { - onBuyTokenScreen { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() - tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + tokenWithTitle(tokenTitle).clickWithAssertion() } } + step("Click on 'Buy' in 'Get token' bottom sheet") { + onGetTokenBottomSheet { buyButton.clickWithAssertion() } + } step("Click on 'Confirm' button in 'Dialog'") { onDialog { confirmButton.clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt index 1f795cadce..5e8dd103e2 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -419,17 +419,17 @@ class MainScreenActionButtonsTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Buy' button is displayed") { - onMainScreen { buyButton.assertIsDisplayed() } + step("Assert 'Add funds' button is displayed") { + onMainScreen { addFundsButton.assertIsDisplayed() } } - step("Click on 'Buy' button") { - onMainScreen { buyButton.performClick() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.performClick() } } - step("Assert 'Buy' screen title is displayed") { - onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() } + step("Assert 'Choose token' screen title is displayed") { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() } } step("Assert token with title: '$tokenTitle' is displayed") { - onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() } + onChooseTokenScreen { tokenWithTitle(tokenTitle).assertIsDisplayed() } } step("Press 'Back' button") { device.uiDevice.pressBack() @@ -478,17 +478,18 @@ class MainScreenActionButtonsTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - step("Assert 'Buy' button is displayed") { - onMainScreen { buyButton.assertIsDisplayed() } + step("Assert 'Add funds' button is displayed") { + onMainScreen { addFundsButton.assertIsDisplayed() } } - step("Click on 'Buy' button") { - onMainScreen { buyButton.performClick() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.performClick() } } - step("Check 'Action is unavailable' dialog") { - checkActionIsUnavailableDialog() + step("Assert 'Choose token' screen opens (Add funds is always available)") { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() } } - step("Click on 'Ok' button") { - onDialog { okButton.performClick() } + step("Press 'Back' to return to main screen") { + device.uiDevice.pressBack() + waitForIdle() } step("Assert 'Swap' button is displayed") { onMainScreen { swapButton.assertIsDisplayed() } @@ -535,17 +536,18 @@ class MainScreenActionButtonsTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - step("Assert 'Buy' button is displayed") { - onMainScreen { buyButton.assertIsDisplayed() } + step("Assert 'Add funds' button is displayed") { + onMainScreen { addFundsButton.assertIsDisplayed() } } - step("Click on 'Buy' button") { - onMainScreen { buyButton.performClick() } + step("Click on 'Add funds' button") { + onMainScreen { addFundsButton.performClick() } } - step("Check 'Action is unavailable' dialog") { - checkActionIsUnavailableDialog() + step("Assert 'Choose token' screen opens (Add funds is always available)") { + onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() } } - step("Click on 'Ok' button") { - onDialog { okButton.performClick() } + step("Press 'Back' to return to main screen") { + device.uiDevice.pressBack() + waitForIdle() } step("Assert 'Swap' button is displayed") { onMainScreen { swapButton.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt index a576c9e327..33185d7a8b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt @@ -51,9 +51,12 @@ class HideTokenTest : BaseTestCase() { dialogContainer.assertIsDisplayed() okButton.clickWithAssertion() } + waitForIdle() } step("Assert token: '$tokenTitle' is not displayed") { - onMainScreen { assertTokenDoesNotExist(tokenTitle) } + flakySafely { + onMainScreen { assertTokenDoesNotExist(tokenTitle) } + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt index ac99d2e42e..32dfdd1b1b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt @@ -2,10 +2,12 @@ package com.tangem.tests.main import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onAddAndManageBottomSheet import com.tangem.screens.onMainScreen import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId @@ -81,8 +83,17 @@ class MainScreenTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - step("Assert 'Add & Manage' button is not displayed") { - onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()} + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButtonNode.assertIsDisplayed() } + } + step("Click 'Add & Manage' button") { + onMainScreen { addAndManageButtonNode.clickWithAssertion() } + } + step("Assert 'Organize tokens' option is not displayed (nothing to organize)") { + onAddAndManageBottomSheet { organizeTokensButton.assertIsNotDisplayed() } + } + step("Assert 'Add tokens' option is displayed") { + onAddAndManageBottomSheet { addTokensButton.assertIsDisplayed() } } } } From d33284be918574e2cebba63022b57a30de03cbf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 May 2026 04:59:02 -0700 Subject: [PATCH 03/16] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 2 +- .../appsflyer/AppsFlyerDeepLinkListener.kt | 4 +- .../AppsFlyerReferralParamsHandler.kt | 35 +++++++-- .../component/impl/DefaultRoutingComponent.kt | 13 ++-- .../AppsFlyerDeepLinkListenerTest.kt | 10 ++- .../AppsFlyerReferralParamsHandlerTest.kt | 77 ++++++++++++++++++- 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 3349a0933d..e998374c66 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - TangemLogger.i("onCreate") + TangemLogger.i("onCreate: data=${intent?.data}, extras=${intent?.extras?.keySet()}") // We need to call it before onCreate to prevent unnecessary activity recreation installAppTheme() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt index b43b358a5c..796b6804ef 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt @@ -14,12 +14,14 @@ class AppsFlyerDeepLinkListener @Inject constructor( override fun onDeepLinking(p0: DeepLinkResult) { when (p0.status) { DeepLinkResult.Status.FOUND -> { - referralParamsHandler.handle(deepLink = p0.deepLink) + referralParamsHandler.handleDeeplink(deepLink = p0.deepLink) } DeepLinkResult.Status.NOT_FOUND -> { + referralParamsHandler.handleNoDeeplink() TangemLogger.i("No deep link found") } DeepLinkResult.Status.ERROR -> { + referralParamsHandler.handleNoDeeplink() TangemLogger.e("Deep link error: ${p0.error}") } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index 25980d4028..70a24bee80 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -7,6 +7,7 @@ import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -23,14 +24,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor( ) { private val mutex = Mutex() - - fun handle(deepLink: DeepLink) { - handle( - deepLinkValue = deepLink.deepLinkValue, - deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1), - deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2), - ) - } + private val deepLinkDeferred = CompletableDeferred() fun handle(params: Map) { handle( @@ -40,6 +34,31 @@ class AppsFlyerReferralParamsHandler @Inject constructor( ) } + fun handleDeeplink(deepLink: DeepLink) { + handle( + deepLinkValue = deepLink.deepLinkValue, + deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1), + deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2), + ) + deepLinkDeferred.complete(deepLink.deepLinkValue) + } + + fun handleNoDeeplink() { + deepLinkDeferred.complete(null) + } + + suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? { + val deeplinkFromCache = appsFlyerStore.getDeeplink(deeplinkSource) + return if (deeplinkFromCache == null) { + val value = when (deeplinkSource) { + AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE + } + deepLinkDeferred.await().takeIf { it == value } + } else { + deeplinkFromCache + } + } + private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue") when (deepLinkValue) { diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 6a7f07951e..6be9860e0c 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -32,7 +32,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.scan.ScanResponse @@ -54,6 +53,7 @@ import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.android.create import com.tangem.sdk.api.BackupServiceHolder import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.common.analytics.appsflyer.AppsFlyerReferralParamsHandler import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.features.scanfails.ScanFailsComponent @@ -70,6 +70,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList", "LargeClass") internal class DefaultRoutingComponent @AssistedInject constructor( @@ -88,7 +90,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, - private val appsFlyerStore: AppsFlyerStore, + private val appsFlyerReferralParamsHandler: AppsFlyerReferralParamsHandler, private val trackingContextProxy: TrackingContextProxy, private val scanFailsComponentFactory: ScanFailsComponent.Factory, private val scanFailsRequesterProxy: ScanFailsRequesterProxy, @@ -212,11 +214,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor( FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING, ) TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled") - if (isHotWalletOnboardingEnabled) { - val tangemPayHotWalletOnboardingDeepLink = appsFlyerStore.getDeeplink( - AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, - ) + val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) { + appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + } TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}") if (tangemPayHotWalletOnboardingDeepLink != null) { val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt index 20f770e186..2a3e53fea0 100644 --- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt @@ -29,15 +29,19 @@ class AppsFlyerDeepLinkListenerTest { @ProvideTestModels fun onDeepLinking(model: OnDeepLinkingModel) = runTest { if (model.shouldHandle) { - every { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } just Runs + every { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) } just Runs + } else { + every { referralParamsHandler.handleNoDeeplink() } just Runs } listener.onDeepLinking(p0 = model.deepLinkResult) if (model.shouldHandle) { - coVerify { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } + coVerify { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) } + verify(inverse = true) { referralParamsHandler.handleNoDeeplink() } } else { - coVerify(inverse = true) { referralParamsHandler.handle(deepLink = any()) } + coVerify(inverse = true) { referralParamsHandler.handleDeeplink(deepLink = any()) } + verify { referralParamsHandler.handleNoDeeplink() } } } diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt index 42ca5e34cc..c2a6c12ac1 100644 --- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt @@ -1,6 +1,8 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase @@ -15,6 +17,7 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest @@ -46,7 +49,7 @@ class AppsFlyerReferralParamsHandlerTest { @ParameterizedTest @ProvideTestModels fun handle(model: HandleDeepLinkModel) = runTest { - handler.handle(deepLink = model.deepLink) + handler.handleDeeplink(deepLink = model.deepLink) if (model.shouldStore) { val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) @@ -165,6 +168,78 @@ class AppsFlyerReferralParamsHandlerTest { data class HandleParamsModel(val params: Map, val shouldStore: Boolean) + @Nested + inner class WaitForDeeplink { + + private val localStore: AppsFlyerStore = mockk(relaxUnitFun = true) + private val localHandler = AppsFlyerReferralParamsHandler( + appsFlyerStore = localStore, + coroutineScope = TestAppCoroutineScope(), + setShouldShowMobileWalletPromoUseCase = mockk { coEvery { this@mockk.invoke(true) } returns Unit.right() }, + ) + + @Test + fun `GIVEN cached deeplink WHEN waitForDeeplink THEN returns cached value`() = runTest { + // GIVEN + coEvery { + localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + } returns "tpay_mobileonboard" + + // WHEN + val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + + // THEN + assertThat(result).isEqualTo("tpay_mobileonboard") + } + + @Test + fun `GIVEN no cache and matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns deeplink value`() = runTest { + // GIVEN + coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null + val deepLink = mockk { + every { deepLinkValue } returns "tpay_mobileonboard" + every { getStringValue(any()) } returns null + } + + // WHEN + localHandler.handleDeeplink(deepLink) + val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + + // THEN + assertThat(result).isEqualTo("tpay_mobileonboard") + } + + @Test + fun `GIVEN no cache and non-matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns null`() = runTest { + // GIVEN + coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null + val deepLink = mockk { + every { deepLinkValue } returns "referral" + every { getStringValue(any()) } returns null + } + + // WHEN + localHandler.handleDeeplink(deepLink) + val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + + // THEN + assertThat(result).isNull() + } + + @Test + fun `GIVEN no cache WHEN handleNoDeeplink then waitForDeeplink THEN returns null`() = runTest { + // GIVEN + coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null + + // WHEN + localHandler.handleNoDeeplink() + val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + + // THEN + assertThat(result).isNull() + } + } + private companion object Companion { const val SUCCESS_REFCODE = "valid_refcode" const val SUCCESS_CAMPAIGN = "valid_campaign" From f747c75708b62c2a7adcee9961c7b826149d6cf2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 May 2026 17:54:05 +0300 Subject: [PATCH 04/16] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 27 +++++++++------- core/res/src/main/res/values-es/strings.xml | 13 +++----- core/res/src/main/res/values-fr/strings.xml | 31 ++++++++++++++----- core/res/src/main/res/values-it/strings.xml | 11 +++---- core/res/src/main/res/values-ja/strings.xml | 29 ++++++++--------- .../src/main/res/values-pt-rBR/strings.xml | 29 ++++++++++------- core/res/src/main/res/values-ru/strings.xml | 16 +++++----- .../src/main/res/values-uk-rUA/strings.xml | 13 +++----- .../src/main/res/values-zh-rCN/strings.xml | 15 +++------ .../src/main/res/values-zh-rTW/strings.xml | 11 +++---- core/res/src/main/res/values/strings.xml | 29 +++++++++-------- gradle/tangem_dependencies.toml | 4 +-- 12 files changed, 120 insertions(+), 108 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 98e3e4817c..b65cf76828 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -91,6 +91,7 @@ Token anlegen Token verwalten Kreditkarte oder Bankkonto + Token erhalten Teile deine Adresse oder dein QR-Code Zwische deinen Portfolios Empfangen @@ -662,6 +663,11 @@ Feedback zu Tangem Eine Transaktion kann nicht gesendet werden Fehler in der Coinbeschreibung + Aktualisiere die Anwendung auf die neueste Version, um die ordnungsgemäße Funktionalität zu gewährleisten + Aktualisierung erforderlich + Update + Bitte aktualisiere die Anwendung auf die neueste Version, um eine einwandfreie Funktion zu gewährleisten. + Aktualisierung erforderlich Nicht genügend Mittel Transaktionsgebühr Es ist ein Fehler aufgetreten @@ -787,6 +793,8 @@ Das Koinos-Netzwerk benötigt Mana als Netzwerkgebühr. Du hast %1$s/%2$s Mana Mana-Level Hinzufügen und Verwalten + Krypto einzahlen oder mit Karte kaufen, um loszulegen + Hol dir deine erste Kryptowährung Um mit der Verfolgung deiner Krypto-Assets und -Transaktionen zu beginnen, füge einen Token hinzu Token verwalten QR-Code scannen, um Geld zu senden oder eine Verbindung zu einer App herzustellen @@ -1073,7 +1081,7 @@ Andere Optionen Deine Schlüssel(private-keys) werden sicher im Inneren der Karte oder Ring generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen. Schlüssel anonym generieren - Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:%s + Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:\n%s Deine Karte oder Ring ist aktiviert und einsatzbereit Erfolgreich! Deine Wallet ist eingerichtet und einsatzbereit! @@ -1210,9 +1218,7 @@ Token organisieren Gruppe löschen %s Unterstützung - Genehmigung erteilen Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast. - Push-Benachrichtigungen sind aktiviert, funktionieren aber erst nach Ihrer Zustimmung. Benachrichtigungen zulassen Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten. Angebote & Updates @@ -1684,11 +1690,13 @@ Karte konnte nicht eingefroren werden. Versuchen Sie es später erneut. Einfrieren Ihre Karte ist eingefroren. + Aufheben Hilfe erhalten Grund: %s %s · %s MCC %s Andere + PIN-Code Nicht nutzbar auf gerooteten Geräten Abgeschlossen Abgelehnt @@ -1697,15 +1705,15 @@ Bedingungen, Gebühren & Limits Bedingungen und Einschränkungen Die Bank hat diese Transaktionsanfrage abgelehnt. - Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung. + Eine Gebühr wird gemäß den Servicetarifen erhoben Die Transaktion wurde vom Händler teilweise oder vollständig storniert Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren. Karte entsperren? Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut. Ihre Karte ist entsperrt. Abhebung - Dies wurde aufgrund regulatorischer Anforderungen durchgeführt. Auszahlungen sind jedoch weiterhin verfügbar. - Ihre Karte wurde deaktiviert + Bei Fragen zu Ihrem Konto, Ihren Daten oder Ihrem Transaktionsverlauf wenden Sie sich bitte an den Support + Ihr Konto wurde geschlossen Auf gerooteten Geräten nicht nutzbar. Verfügbares Guthaben KYC vom Hauptbildschirm ausblenden @@ -1739,7 +1747,6 @@ Karte zu Google Pay hinzufügen Karte zu Apple Pay hinzufügen Pin Code - Teile Deine Adresse mit oder zeig den QR-Code. Es wurden technische Probleme festgestellt. Bitte versuche es später erneut oder kontaktiere den Support. Empfangen ist jetzt nicht verfügbar Karte neu ausstellen @@ -1748,7 +1755,6 @@ Kartenname Aufdecken Details anzeigen - Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte. Kartendetails Bitte versuche es später noch einmal. Karte entsperren @@ -1766,6 +1772,7 @@ Ändern Aktuelles Limit Ihr Tageslimit konnte nicht geladen werden. Bitte versuchen Sie es erneut. + Neu laden und es erneut versuchen Tageslimit nicht verfügbar Sie können es jederzeit wieder ändern Tageslimit ist festgelegt @@ -1827,7 +1834,7 @@ Unerreichte Privatsphäre Verknüpfen Sie eine Zahlungskarte Wir richten eine Wallet ein. - Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten + Holen Sie sich Ihre Tangem Pay Karte Bezahlen mit Zahlungskonto Tangem Pay sitzung abgelaufen @@ -1851,7 +1858,6 @@ Karte oder Ring verwenden, um die Sitzung zu verlängern Karte oder Ring verwenden, um die Sitzung zu verlängern Zugang wiederherstellen - Zugang wiederherstellen Tangem Pay sitzung abgelaufen Nutzen Sie USDC für alltägliche Zahlungen Tangem Pay ist vorübergehend nicht erreichbar. @@ -1861,7 +1867,6 @@ Tauschen Sie beliebige Assets in USDC Polygon um Aus Ihrer Tangem Wallet USDC im Polygon - Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar Bitte beachten Sie Ihr PIN-Code diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 298ebb9881..84c8d58f96 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -370,6 +370,7 @@ Seleccione una acción Vender Enviar + Enviar: Error al enviar la transacción El servidor no está disponible, por favor inténtelo de nuevo más tarde Compartir @@ -1609,15 +1610,15 @@ Términos, tarifas y límites Términos y límites El banco rechazó esta solicitud de transacción. - Esta tarifa cubre el costo de procesar tu transferencia. + Se cobra una comisión de acuerdo con las tarifas de servicio La transacción fue revertida parcial o totalmente por el comerciante Sigue usando tu dinero. Puedes congelarlo en cualquier momento. ¿Descongelar tu tarjeta? No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde. Tu tarjeta está descongelada. Retirada - Esto se hizo debido a requisitos regulatorios. Sin embargo, los retiros siguen estando disponibles. - Su tarjeta ha sido desactivada + Para consultas sobre su cuenta, datos o historial de transacciones, contacte con el soporte + Su cuenta ha sido cerrada No se puede usar en un dispositivo rooteado Saldo Ocultar verificación de la pantalla @@ -1650,7 +1651,6 @@ Añadir tarjeta a Google Pay Añade tu tarjeta a Apple Pay Código PIN - Comparte tu dirección o muestra el código QR Se detectaron problemas técnicos. Inténtelo de nuevo más tarde o póngase en contacto con el servicio de asistencia. Recepción no disponible ahora Reemitir tarjeta @@ -1658,7 +1658,6 @@ Caracteres no válidos Mostrar Mostrar detalles - Intercambia cualquier activo de tu portafolio por una tarjeta Detalles de la tarjeta Por favor, inténtalo de nuevo más tarde Descongelar tarjeta @@ -1719,7 +1718,7 @@ Paga exactamente lo que ves Se creará una cuenta de pago separada sin divulgar tus direcciones y activos Privacidad inigualable - Obtén tu tarjeta Tangem Pay gratuita en minutos + Obtén tu tarjeta Tangem Pay en minutos Cuenta de pago Tangem Pay sesión expirada PIN no válido: evitar secuencias o repeticiones @@ -1741,7 +1740,6 @@ Usa la tarjeta o el anillo para renovar la sesión Usa la tarjeta o el anillo para renovar la sesión Restablecer acceso - Restablecer acceso Tangem Pay sesión expirada Usa USDC para pagos cotidianos Tangem Pay no está disponible temporalmente. @@ -1751,7 +1749,6 @@ Intercambia cualquier activo por USDC Polygon Desde tu Tangem Wallet USDC en Polygon - Haga clic en el botón de abajo para restaurar el acceso Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain Polygon ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras Tenga en cuenta Tu código PIN diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 68600b3f36..c3f3bb8e2c 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -80,6 +80,7 @@ Ajouter des jetons Sélectionnez le jeton que vous souhaitez recevoir Sélectionnez le jeton que vous souhaitez échanger + Ajouter des jetons Choisissez le réseau Ajouter un jeton personnalisé Gérer les jetons @@ -367,6 +368,7 @@ Sélectionnez une action Vendre Envoyer + Vous envoyez : Échec d\'envoi de la transaction Le serveur n\'est pas disponible, veuillez réessayer plus tard Partager @@ -549,6 +551,7 @@ Fournisseur Meilleur taux Liste d’avertissement de la FCA + Prestataire pour l\'échange Meilleur choix Fournisseur figurant sur la liste d\'avertissement de la FCA Disponible jusqu\'à %s @@ -711,6 +714,7 @@ Limite de Mana Le réseau Koinos nécessite du Mana pour les frais de réseau. Vous avez %1$s/%2$s Mana Quantité de Mana + Ajouter & gérer Pour commencer à suivre vos actifs et transactions crypto, ajoutez des jetons Gérer les jetons Pour accéder à tous les réseaux, vous devez scanner la carte @@ -1047,16 +1051,29 @@ Cette transaction a déjà été traitée. Aucune autre action n\'est requise. Recherche des meilleurs tarifs... Instantané + La vérification est gratuite et prend généralement entre 1 et 2 minutes + Tangem n\'a pas accès à vos données personnelles, vous les partagez directement au prestataire agréé + La vérification vous donne un accès complet aux futures transactions avec ce prestataire + Sélectionner une autre méthode + Conformément aux exigences réglementaires locales, %@ exige une vérification d\'identité. + Vérification d\'identité requise par le prestataire de paiement + Passer la vérification + Ce qui est important En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur Le service est fourni par un prestataire externe. Tangem n\'est pas responsable. Le montant de l\'achat ne doit pas dépasser %s Le montant à acheter doit être au moins %s + Si le montant cumulé des transactions dépasse %1s, une vérification d\'identité via %2s pourrait être requise + Si le montant cumulé des transactions dépasse l\'équivalent de %1s, une vérification d\'identité via %2s pourrait être requise + En appuyant sur Acheter, vous acceptez %1s %2s et %3s. Aucun fournisseur disponible pour cette devise Le plus rapide Payer avec Mode de paiement Disponible jusqu\'à %s Disponible à partir de %s + Les cartes émises aux États-Unis et au Royaume-Uni ne peuvent pas être traitées par ce moyen. Le prestataire pourrait exiger une vérification d\'identité supplémentaire + Exigences du prestataire %d fournisseur %d fournisseurs @@ -1456,6 +1473,7 @@ Une transaction entrante d\'au moins de %1$s est requise pour continuer Fonds insuffisants En approuvant, vous autorisez le contrat intelligent à utiliser vos jetons dans de futures transactions. + Mode détaillé Taux fixe Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Échange en cours @@ -1463,6 +1481,7 @@ Nouveau fournisseur d\'échange disponible ! Recherchez n’importe quel token, même s’il ne figure pas encore dans votre liste. Utilisez la recherche pour trouver ce dont vous avez besoin. + Mode simplifié Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes Assistance 24 heures sur 24 Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement @@ -1534,15 +1553,15 @@ Conditions, frais et limites Conditions et limites La banque a rejeté cette demande de transaction. - Ces frais couvrent le coût du traitement de votre virement. + Des frais sont prélevés conformément aux tarifs de service La transaction a été partiellement ou totalement annulée par le commerçant Continuez à utiliser votre argent. Vous pouvez le geler à tout moment. Dégeler votre carte ? Échec du dégel de la carte. Réessayez plus tard. Votre carte est dégelée. Retrait - Cela a été fait conformément aux exigences réglementaires. Toutefois, les retraits restent disponibles. - Votre carte a été désactivée + Pour toute question concernant votre compte, vos données ou votre historique de transactions, veuillez contacter le support + Votre compte a été fermé Impossible à utiliser sur un appareil rooté Solde Masquer la vérification de l\'écran @@ -1574,7 +1593,6 @@ Ajouter une carte à Google Pay Ajouter la carte à Apple Pay code PIN - Partagez votre adresse ou montrez le QR code Problèmes techniques détectés. Veuillez réessayer plus tard ou contacter le service d\'assistance. Réception indisponible pour le moment Réémettre la carte @@ -1582,7 +1600,6 @@ Caractères non valides Révéler Afficher les détails - Échangez n\'importe quel actif de votre portefeuille contre une carte Détails de la carte Veuillez réessayer plus tard Dégeler la carte @@ -1643,7 +1660,7 @@ Payez exactement ce que vous voyez Un compte de paiement séparé sera créé sans divulguer vos adresses et actifs Confidentialité inégalée - Obtenez votre carte Tangem Pay gratuite en quelques minutes + Obtenez votre carte Tangem Pay en minutes Compte de paiement Tangem Pay session expirée Code PIN invalide : évitez les séquences ou les répétitions @@ -1665,7 +1682,6 @@ Utilisez carte ou bague pour renouveler la session Utilisez carte ou bague pour renouveler la session Restaurer l\'accès - Restaurer l\'accès Tangem Pay session expirée Utilisez USDC pour les paiements quotidiens Tangem Pay est temporairement indisponible @@ -1675,7 +1691,6 @@ Échangez n\'importe quel actif contre USDC Polygon Depuis votre Tangem Wallet USDC sur Polygon - Cliquez sur le bouton ci-dessous pour restaurer l\'accès Les fonds des achats remboursés ne seront pas retournés à votre solde sur Polygon ni disponibles pour un retrait, mais resteront sur votre solde de carte pour vos achats Veuillez noter Votre code PIN diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 066a70f131..83d4fa8fa7 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -94,15 +94,15 @@ Termini, commissioni e limiti Termini e limiti La banca ha rifiutato questa richiesta di transazione. - Questa commissione copre il costo della gestione del tuo trasferimento. + Viene addebitata una commissione in base alle tariffe del servizio La transazione è stata parzialmente o totalmente stornata dal commerciante Continua a usare i tuoi soldi. Puoi congelarli in qualsiasi momento. Sbloccare la tua carta? Impossibile sbloccare la carta. Riprova più tardi. La tua carta è sbloccata. Prelievo - Questo è stato fatto a causa dei requisiti normativi. Tuttavia, i prelievi sono ancora disponibili. - La tua carta è stata disattivata + Per domande su account, dati o cronologia delle transazioni, contatta il supporto + Il tuo account è stato chiuso Saldo Nascondi verifica dalla schermata Aggiungi fondi @@ -132,14 +132,12 @@ Tutto pronto! La tua carta è pronta per l\'uso. Aggiungi carta a Google Pay Aggiungi carta ad Apple Pay - Condividi il tuo indirizzo o mostra il QR code Ricezione non disponibile al momento Riemettere la carta Sono consentite solo lettere e numeri Caratteri non validi Rivela Mostra dettagli - Scambia qualsiasi asset nel tuo portafoglio con una carta Dettagli carta Per favore riprova più tardi Sblocca carta @@ -194,7 +192,7 @@ Paga esattamente quello che vedi Verrà creato un conto di pagamento separato senza divulgare i tuoi indirizzi e asset Privacy senza rivali - Ottieni la tua carta Tangem Pay gratuita in pochi minuti + Ottieni la tua carta Tangem Pay in pochi minuti Conto di pagamento Tangem Pay sessione scaduta PIN non valido: evitare sequenze o ripetizioni @@ -223,7 +221,6 @@ Converti qualsiasi asset in USDC Polygon Dal tuo Tangem Wallet USDC sulla Polygon - Fare clic sul pulsante in basso per ripristinare l\'accesso I fondi degli acquisti rimborsati non verranno restituiti al tuo saldo on-chain Polygon né saranno disponibili per il prelievo, ma rimarranno sul saldo della tua carta per gli acquisti Attenzione Il tuo codice PIN diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e7eb4ac259..561ce263e1 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -393,6 +393,7 @@ 送金中 送金済み サーバーが利用できません。しばらくしてからもう一度お試しください。 + セッションの有効期限が切れました 共有 リンクを共有 詳細を非表示 @@ -776,6 +777,8 @@ Koinosネットワークでは、ネットワーク手数料としてManaが必要です。あなたは%1$s / %2$sManaを持っています。 Manaレベル 追加・管理 + 暗号資産を入金またはカードで購入 + 入金して、運用や取引を始めましょう。 暗号資産および取引の追跡を開始するには、トークンを追加してください トークンの管理 QRコードをスキャンして送金するか、アプリに接続します。 @@ -1187,9 +1190,7 @@ トークンを整理する グループ解除 %sサポート - 許可する - プッシュ通知は有効になっていますが、端末の設定で通知を許可するまで動作しません。 - プッシュ通知は有効になっていますが、許可するまで機能しません + プッシュ通知は有効ですが、許可するまで動作しません 通知を許可する 製品ニュース、限定オファー、アクティビティのリマインダー。 オファー・最新情報 @@ -1628,7 +1629,7 @@ プロバイダーの利用体験を評価してください フィードバックを入力してください フィードバックを送信 - ご利用体験に影響した点は\n何ですか? + ご利用中に気になった点を\n教えてください スワップ スワップ中… 受け取り先 @@ -1671,15 +1672,15 @@ 利用規約・手数料・利用制限 利用規約と手数料 銀行がこの取引リクエストを拒否しました。 - この手数料は、送金処理にかかるコストをカバーするためのものです。 + 手数料はサービス料金に基づいて請求されます この取引は加盟店により一部または全額取り消されました 資金は引き続き使用できます。いつでも一時停止できます。 カードの一時停止を解除しますか? カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。 カードの凍結が解除されました 出金 - 規制上の要件により無効化されましたが、出金は引き続き可能です。 - カードが無効化されました + アカウント、データ、または取引履歴に関するご質問は、サポートまでご連絡ください + あなたのアカウントは閉鎖されました Root化された端末では使用できません 利用可能残高 メイン画面からKYCを非表示にする @@ -1713,7 +1714,6 @@ Google Payにカードを追加する Apple Payにカードを追加する PINコード - アドレスを共有するか、QRコードを表示してください。 技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。 現在、受け取りは利用できません カードを交換する @@ -1722,7 +1722,6 @@ カード名 表示 詳細を表示 - ポートフォリオ内のあらゆる資産をカードと交換 カードの詳細 しばらくしてからもう一度お試しください カードの一時停止を解除 @@ -1800,7 +1799,7 @@ 他に類を見ないプライバシー そして支払いカードを連携します ウォレットを設定します - 無料のTangem Payカードを数分でゲットしましょう + Tangem Pay カードをすぐに手に入れよう Payサポート 支払いアカウント Tangem Pay セッションの有効期限が切れました @@ -1824,17 +1823,15 @@ カードまたはリングでセッションを更新してください カードまたはリングでセッションを更新してください セッションを更新 - セッションを更新 Tangem Pay セッションの有効期限が切れました 日常の支払いにUSDCを利用 Tangem Payは現在一時的に利用できません。 Tangem Pay USDC Polygon をアカウントのアドレスに送信 別のウォレットまたは取引所から - 任意の資産を USDC Polygon にスワップ - Tangem ウォレットから + ウォレットの暗号資産を使って、決済アカウントにチャージできます + Tangemウォレットからスワップ Polygonネットワーク上のUSDC - 下のボタンをクリックしてアクセスを復元してください 返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。 ご注意ください PINコード @@ -2346,6 +2343,10 @@ 利息モード限定オファー APY 3倍 APYブーストを有効にする + ボーナスを有効にする + 詳細は取引履歴をご確認ください + 利息モードのボーナスが支払われました + ボーナス獲得まであと%1$s日 30日間APYブーストの対象です。利用規約が適用されます。詳細はこちら。 初めて利息モードを有効にすると、最初の30日間は最大3倍の利回りを獲得できます。 初月APRボーナス diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index de8f5ba626..d3bf24d22d 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -91,6 +91,7 @@ Adicionar token personalizado Gerenciar tokens Cartão de crédito ou conta bancária + Adicionar token Compartilhe seu endereço ou código QR. Entre seus portfólios Você recebe @@ -227,7 +228,7 @@ %s fracassado Ativar Adicionar - Adicionar fundos + Depositar Adicionar ao portfólio Adicionar token Adicionar tokens @@ -662,6 +663,11 @@ Feedback Tangem Não foi possível enviar uma transação. Erro na descrição da moeda + Atualize o aplicativo para a versão mais recente para garantir o funcionamento correto. + Atualização necessária + Atualizar + Por favor, atualize o aplicativo para a versão mais recente para garantir o funcionamento correto. + Atualização necessária Fundos insuficientes Taxa de transação Ocorreu um erro. @@ -787,6 +793,8 @@ A rede Koinos exige Mana para o pagamento das taxas de rede. Você tem %1$s/%2$s Mana Nível de mana Adicionar e gerenciar + Compre ou receba criptomoedas para começar a usar sua carteira. + Adquira suas primeiras criptomoedas. Para começar a rastrear seus criptoativos e transações, adicione tokens. Gerenciar tokens Leia o código QR para enviar fundos ou conectar-se a um aplicativo @@ -1073,7 +1081,7 @@ Outras opções Suas chaves serão geradas com segurança dentro do chip. Não há frase mnemônica, o que significa que ninguém pode exportá-las ou roubá-las. Gere chaves de forma privada - Ao continuar, você concorda com os termos. %s + Ao continuar, você concorda com os termos.\n%s Seu cartão está ativado e pronto para uso. Sucesso! Sua carteira está configurada e pronta para uso! @@ -1210,9 +1218,7 @@ Organizar tokens Desagrupar %s suporte - Conceder permissão As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo. - As notificações push estão ativadas, mas não funcionarão até que você conceda permissão. Permitir notificações Novidades sobre produtos, ofertas exclusivas e lembretes de atividades. Ofertas e atualizações @@ -1684,11 +1690,13 @@ Não foi possível bloquear o cartão. Tente novamente mais tarde. Congelar Seu cartão está bloqueado. + Descongelar Obtenha ajuda Razão: %s %s · %s MCC %s Outro + Código PIN Não é possível usar em dispositivos com root. Concluído Recusado @@ -1697,15 +1705,15 @@ Termos, taxas e limites Termos e Limites O banco rejeitou esta solicitação de transação. - Essa taxa destina-se a cobrir os custos de processamento da sua transferência. + Uma taxa é cobrada de acordo com as tarifas de serviço A transação foi parcial ou totalmente revertida pelo comerciante. Continue usando seu dinheiro. Você pode congelar a qualquer momento. Descongelar seu cartão? Não foi possível desbloquear o cartão. Tente novamente mais tarde. Seu cartão foi desbloqueado. Retirada - Isso foi feito devido a requisitos regulatórios. No entanto, saques ainda estão disponíveis. - Seu cartão foi desativado + Para dúvidas sobre sua conta, dados ou histórico de transações, entre em contato com o suporte + Sua conta foi encerrada Não é possível usar em dispositivos com root. Saldo disponível Ocultar KYC da tela principal @@ -1739,7 +1747,6 @@ Adicionar cartão ao Google Pay Adicionar cartão ao Apple Pay Código PIN - Compartilhe seu endereço ou mostre o código QR. Problemas técnicos detectados. Tente novamente mais tarde ou entre em contato com o suporte. Receber indisponível agora Substituir cartão @@ -1748,7 +1755,6 @@ Nome do cartão Revelar Mostrar detalhes - Troque qualquer ativo da sua carteira por um cartão. Detalhes do cartão Por favor, tente novamente mais tarde. Descongelar cartão @@ -1766,6 +1772,7 @@ Mudar Limite atual Não foi possível carregar seu limite diário. Tente novamente. + Recarregue a página para tentar novamente. Limite diário indisponível Você pode alterar isso novamente quando quiser. O limite diário está definido. @@ -1827,7 +1834,7 @@ Privacidade incomparável E vincule um cartão de pagamento a ele. Vamos configurar uma carteira. - Obtenha seu cartão Tangem Pay gratuito em minutos. + Obtenha seu cartão Tangem Pay em minutos Suporte de Pay Conta de pagamento Tangem Pay sessão expirada @@ -1851,7 +1858,6 @@ Use o cartão ou anel para renovar a sessão Use o cartão ou anel para renovar a sessão Restaurar acesso - Restaurar acesso Tangem Pay sessão expirada Use USDC para pagamentos do dia a dia. O serviço Tangem Pay está temporariamente inacessível. @@ -1861,7 +1867,6 @@ Troque qualquer ativo por USDC Polygon Da sua Tangem Wallet USDC na rede Polygon - Clique no botão abaixo para restaurar o acesso. Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras Observe Seu código PIN diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0218005218..a230e50164 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -231,7 +231,7 @@ Аккаунты Активировать Добавить - Добавить средств + Пополнить Добавить в портфель Добавить токен Добавьте токены @@ -410,6 +410,7 @@ Выберите действие Продать Отправить + Отправка: Не удалось отправить транзакцию Сервер недоступен, повторите попытку позднее Поделиться @@ -782,6 +783,8 @@ Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana Уровень маны Добавить и управлять + Купите криптовалюту или переведите её на свой кошелёк. + Пополните кошелёк Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены Управление токенами Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению. @@ -1684,15 +1687,15 @@ Тарифы и полные условия Тарифы и лимиты Банк отклонил транзакцию - Эта комиссия покрывает стоимость обработки вашего перевода. + Комиссия взимается в соответствии с тарифами обслуживания Транзакция частично или полностью возвращена продавцом Продолжайте пользоваться картой, заморозить всегда успеете Разморозить карту? Не удалось разморозить карту, попробуйте еще раз Карта разморожена Вывод средств - Это произошло из-за регуляторных требований. Вывод средств по-прежнему доступен. - Карта была деактивирована + По вопросам данных или истории транзакций, обратитесь в поддержку + Аккаунт закрыт Запрещено использовать на root-устройствах Баланс Скрыть KYC с главной @@ -1726,7 +1729,6 @@ Добавьте карту в Google Pay Добавить карту в Apple Pay ПИН-код - Скопируйте свой адрес или покажите QR Техническая ошибка. Попробуйте позже или обратитесь в поддержку. Пополнение недоступно Перевыпустить карту @@ -1734,7 +1736,6 @@ Недопустимые символы Показать Реквизиты - Пополните карту любым активом через обмен Реквизиты Пожалуйста, попробуйте позже Разморозить карту @@ -1816,7 +1817,6 @@ Используйте карту или кольцо для обновления сессии Используйте карту или кольцо для обновления сессии Обновить сессию - Обновить сессию Tangem Pay · Cессия истекла Оплачивайте ежедневные покупки в USDC Tangem Pay временно недоступен @@ -1826,7 +1826,6 @@ Обменяйте любой актив на USDC Polygon Из вашего кошелька Tangem USDC в сети Polygon - Нажмите на кнопку ниже, чтобы восстановить доступ При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок Обратите внимание Ваш PIN-код @@ -2071,6 +2070,7 @@ Сумма получения не может быть менее %s Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна + Для пользователей из Великобритании: некоторые провайдеры не авторизованы FCA Великобритании. Вам следует избегать взаимодействия с ними. Предупреждающий список FCA Сервис временно недоступен Сумма для обмена должна быть не более %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index e8d7f2b393..e2f072ac6b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -384,6 +384,7 @@ Оберіть дію Продати Надіслати + Відправка: Не вдалося надіслати транзакцію Сервер недоступний, спробуйте пізніше Поширити @@ -1604,15 +1605,15 @@ Умови, комісії та ліміти Умови та обмеження Банк відхилив цей запит на транзакцію. - Ця комісія покриває витрати на обробку вашого переказу. + Комісія стягується відповідно до тарифів обслуговування Транзакцію було частково або повністю скасовано продавцем Продовжуйте користуватися карткою. Заморозити можна в будь-який момент. Розморозити картку? Не вдалося розморозити картку. Спробуйте пізніше. Картку розморожено. Виведення коштів - Це було зроблено відповідно до регуляторних вимог. Виведення коштів усе ще доступне. - Вашу картку було деактивовано + З питань щодо даних або історії транзакцій зверніться до служби підтримки + Ваш обліковий запис було закрито Заборонено використовувати на root-пристроях Баланс Приховати KYC з головного екрана @@ -1644,7 +1645,6 @@ Додайте картку до Google Pay Додайте свою картку в Apple Pay ПІН-код - Поділіться своєю адресою або покажіть QR-код Виявлено технічні проблеми. Будь ласка, спробуйте пізніше або зверніться до служби підтримки. Поповнення наразі недоступне Перевипустити картку @@ -1652,7 +1652,6 @@ Неприпустимі символи Показати Показати деталі - Обміняйте будь-який актив у вашому портфелі на картку Реквізити картки Будь ласка, спробуйте пізніше Розморозити картку @@ -1713,7 +1712,7 @@ Платіть стільки, скільки бачите Буде створено окремий платіжний рахунок без розкриття ваших адрес та активів Неперевершена конфіденційність - Отримайте безкоштовну картку Tangem Pay за лічені хвилини + Отримайте картку Tangem Pay за лічені хвилини Платіжний акаунт Tangem Pay · Сесія закінчилася Слабкий ПІН: не використовуйте повторів або послідовностей. @@ -1735,7 +1734,6 @@ Використайте картку або кільце для поновлення сесії Використайте картку або кільце для поновлення сесії Відновити доступ - Відновити доступ Tangem Pay · Сесія закінчилася Використовуйте USDC для щоденних платежів Tangem Pay тимчасово недоступний @@ -1745,7 +1743,6 @@ Обміняйте будь-який актив на USDC Polygon З вашого Tangem Wallet USDC у Polygon - Натисніть кнопку нижче, щоб відновити доступ Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок. Зверніть увагу Ваш PIN-код diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index 9af010f72a..4d111438f9 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -393,6 +393,7 @@ 发送中 发送 服务器不可用,请稍后再试。 + 会话已过期 分享 分享链接 显示更少 @@ -1187,9 +1188,7 @@ 整理代币 取消分组 %s 支持 - 授予权限 推送通知已启用,但需要您在设备设置中允许通知才能正常工作。 - 推送通知已启用,但需要您授予权限才能生效。 允许通知 产品资讯、独家优惠和活动提醒。 优惠与更新 @@ -1671,15 +1670,15 @@ 条款、费用和限制 条款和限制 银行拒绝了这项交易请求。 - 这笔费用用于支付您办理转账时的费用。 + 费用按服务费率收取 商家部分或全部撤销了交易 继续使用您的资金。您可以随时冻结资金。 要解冻您的卡片? 卡片解冻失败,请稍后再试。 您的卡片已解冻。 提款 - 这是根据监管要求执行的。不过,提现仍然可用。 - 您的卡已停用 + 如需咨询账户、数据或交易记录,请联系支持团队 + 您的账户已被关闭 无法在已root的设备上使用 可用余额 从主屏幕隐藏 KYC 页面 @@ -1713,7 +1712,6 @@ 将卡片添加到 Google Pay 将卡片添加到 Apple Pay PIN码 - 分享您的地址或出示二维码 检测到技术问题。请稍后再试或联系技术支持。 目前无法接收 重新发行卡片 @@ -1722,7 +1720,6 @@ 卡片名称 显示 显示详情 - 将您投资组合中的任何资产互换到卡片 卡片详情 请稍后再试。 解冻卡片 @@ -1800,7 +1797,7 @@ 无与伦比的隐私保护 并将其与支付卡关联。 我们将设置一个钱包。 - 几分钟内即可获得免费的 Tangem Pay 卡 + 立即获取你的 Tangem Pay 卡 支付支持 支付账户 Tangem Pay 会话已过期 @@ -1824,7 +1821,6 @@ 用卡或戒指续期会话 用卡或戒指续期会话 恢复访问权限 - 恢复访问权限 Tangem Pay 会话已过期 使用 USDC 进行日常支付 Tangem Pay暂时无法使用。 @@ -1834,7 +1830,6 @@ 將任何資產兌換為 USDC Polygon 從您的 Tangem 錢包 Polygon网络上的 USDC - 点击下方按钮恢复访问权限 您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。 请注意 您的PIN码 diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 7f24d3f492..3db7971b52 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -337,15 +337,15 @@ 條款、費用與限制 條款與限制 銀行拒絕了此交易請求。 - 此費用用於支付處理您轉帳的成本。 + 費用依服務費率收取 該交易已被商家部分或全額撤銷 繼續使用您的資金。您可以隨時凍結。 解凍您的卡片? 無法解凍卡片。請稍後再試。 您的卡片已解凍。 提現 - 这是根据监管要求执行的。不过,提现仍然可用。 - 您的卡已停用 + 如需查詢帳戶、資料或交易記錄,請聯絡客服支援 + 您的帳戶已被關閉 在主畫面隱藏身份驗證 添加资金 充值选项 @@ -374,12 +374,10 @@ 全部完成!您的卡片已準備就緒。 將卡片添加到 Google Pay 添加卡片到 Apple Pay - 分享您的地址或显示二维码 暫時無法接收 重新发行卡片 显示 顯示詳情 - 將您投資組合中的任何資產兌換成卡片 卡片详情 解凍卡片 提现 @@ -420,7 +418,7 @@ 所見即所付 將創建單獨的支付帳戶,且不會透露您的地址和資產 無與倫比的隱私 - 在幾分鐘內獲得免費的 Tangem Pay 卡 + 立即獲取你的 Tangem Pay 卡 付款帳戶 Tangem Pay 工作階段已過期 這將產生一組新的卡片資料。您的舊資料將停止使用。此操作無法復原。 @@ -439,7 +437,6 @@ 从其他钱包或交易所 将任何资产兑换为 USDC Polygon 从您的 Tangem 钱包 - 點擊下方按鈕以恢復存取權限 您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。 請注意 您的PIN码 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index af23d2743e..7806491a95 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ Add custom token Manage tokens Credit card or bank account + Fund token Share your address or QR-code Between your portfolios You receive @@ -663,6 +664,11 @@ Tangem feedback Can\'t send a transaction Coin description error + Update the application to the latest version to ensure proper functionality + Update Needed + Update + Please update the application to the latest version to ensure proper functionality. + Update Required Not enough funds Transaction fee An error occurred @@ -788,8 +794,8 @@ The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana Mana level Add & Manage - Deposit crypto or buy with card to get started - Add funds to start earning and trading + Buy or receive crypto to start using your wallet. + Get your first crypto To begin tracking your crypto assets and transactions, add tokens Manage tokens Scan QR code to send funds or connect to an app @@ -1213,9 +1219,7 @@ Organize tokens Ungroup %s support - Grant permission - Push Notifications are enabled but won\'t work until you allow notifications in your device settings - Push Notifications are enabled but won\'t work until you grant permission + Push Notifications are enabled but won\'t work until you allow them Allow notifications Product news, exclusive offers, and activity reminders. Offers & Updates @@ -1687,11 +1691,13 @@ Failed to freeze the card. Try again later. Freeze Your card is frozen. + Unfreeze Get Help Reason: %s %s · %s MCC %s Other + PIN-code Unable to use on rooted devices Completed Declined @@ -1700,15 +1706,15 @@ Terms, Fees & Limits Terms and fees The bank rejected this transaction request. - This fee goes to cover the cost of handling your transfer. + A fee is charged in accordance with the service tariffs The transaction was partially or fully reversed by the merchant Keep using your money. You can freeze anytime. Unfreeze your card? Failed to unfreeze the card. Try again later. Your card is unfrozen. Withdrawal - This was done due to regulatory requirements. Anyway withdrawals are still available. - Your card was deactivated + For questions about account, data or transaction history, please contact support + Your account has been closed Unable to use on rooted device Available balance Hide KYC from main screen @@ -1742,7 +1748,6 @@ Add card to Google Pay Add card to Apple Pay PIN code - Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now Replace card @@ -1751,7 +1756,6 @@ Card name Reveal Show details - Swap any asset in your portfolio for card Card details Please try again later Unfreeze Card @@ -1769,6 +1773,7 @@ Change Current limit We couldn\'t load your daily limit. Please try again. + Reload to try again Daily limit unavailable You can change it again anytime you like Daily limit is set @@ -1830,7 +1835,7 @@ Unrivaled privacy And link a payment card to it We\'ll set up a wallet - Get your free Tangem Pay Card in minutes + Get your Tangem Pay Card in minutes Pay Support Payment account Payment account session expired @@ -1854,7 +1859,6 @@ Use your card or ring to renew session Use your card or ring to renew session Renew session - Renew session Payment account session expired Use USDC for everyday payments Tangem Pay is temporarily unreachable @@ -1864,7 +1868,6 @@ Use crypto from your wallet to top up your payment account Swap from Tangem Wallet USDC on Polygon network - Click the button below to restore access Funds from refunded purchases won’t be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases Please note Your PIN code diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8f5b28ffc0..0ab9e14901 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.39-1530" +tangemBlockchainSdk = "releases-5.39-1533" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.39-622" +tangemCardSdk = "releases-5.39-623" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From f000c0c6876e9c7794ebb8cf2323ee05bfeaecbe Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 14:20:53 +0500 Subject: [PATCH 05/16] Updated on 2026-08-14 --- .../entity/PaymentAccountStatusValueDM.kt | 2 + .../DefaultTangemPayCryptoCurrencyFactory.kt | 55 ----------- .../PaymentAccountStatusValueDMConverter.kt | 6 +- .../tangem/data/pay/di/TangemPayDataModule.kt | 15 +-- ....kt => DefaultTangemPayCurrencyFactory.kt} | 24 ++--- .../DefaultPaymentAccountStatusFetcher.kt | 14 ++- ...aymentAccountStatusValueDMConverterTest.kt | 6 +- .../account/PaymentAccountStatusValue.kt | 93 +++++++++++++------ .../tokens/BalanceFetchingOperations.kt | 15 ++- .../tokens/wallet/WalletBalanceFetcher.kt | 2 + .../pay/TangemPayCryptoCurrencyFactory.kt | 12 --- .../domain/pay/TangemPayCurrencyFactory.kt | 29 ++++++ .../domain/GetMultiWalletWarningsFactory.kt | 11 +-- .../domain/GetWalletNotificationsFactory.kt | 5 +- 14 files changed, 143 insertions(+), 146 deletions(-) delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt rename data/visa/src/main/kotlin/com/tangem/data/pay/entity/{TangemPayCurrencyFactory.kt => DefaultTangemPayCurrencyFactory.kt} (69%) delete mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCurrencyFactory.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index f9047d1e43..253484bf99 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -45,6 +45,7 @@ sealed interface PaymentAccountStatusValueDM { @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + @Json(name = "fiat_rate") val fiatRate: BigDecimal?, @Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal, @Json(name = "cards") val cards: List, ) : PaymentAccountStatusValueDM @@ -58,6 +59,7 @@ sealed interface PaymentAccountStatusValueDM { @NameLabel("deactivated_account") data class DeactivatedAccount( @Json(name = "deactivated_account") val marker: Boolean = true, + @Json(name = "fiat_rate") val fiatRate: BigDecimal?, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, ) : PaymentAccountStatusValueDM diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt deleted file mode 100644 index ca7b011917..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.data.pay - -import arrow.core.Either -import arrow.core.Either.Companion.catch -import com.tangem.blockchain.blockchains.ethereum.Chain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.core.error.UniversalError -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.pay.entity.TangemPayCurrencyFactory -import com.tangem.data.pay.util.TangemPayErrorConverter -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory -import com.tangem.utils.logging.TangemLogger -import javax.inject.Inject - -private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory" - -@Deprecated("Use TangemPayCurrencyFactory instead") -internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( - excludedBlockchains: ExcludedBlockchains, - private val errorConverter: TangemPayErrorConverter, -) : TangemPayCryptoCurrencyFactory { - - private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - CryptoCurrencyFactory(excludedBlockchains) - } - private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - NetworkFactory(excludedBlockchains) - } - - override fun create(userWallet: UserWallet, chainId: Int): Either { - return catch { - val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" } - val blockchain = requireNotNull(chain.blockchain) - val network = networkFactory.create( - blockchain = blockchain, - extraDerivationPath = null, - userWallet = userWallet, - ) - cryptoCurrencyFactory.createToken( - network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID), - name = TangemPayCurrencyFactory.TOKEN_NAME, - symbol = TangemPayCurrencyFactory.TOKEN_NAME, - contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS, - decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS, - ) - }.mapLeft { exception -> - TangemLogger.withTag(TAG).e("Error", exception) - errorConverter.convert(exception) - } - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 9eae098385..1b6a9681c8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -1,7 +1,6 @@ package com.tangem.data.pay.converter import arrow.core.getOrElse -import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.CardDisplayName @@ -11,6 +10,7 @@ import com.tangem.domain.models.pay.TangemPayCardLimit import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayCurrencyFactory import javax.inject.Inject import javax.inject.Singleton @@ -42,6 +42,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( fiatBalance = value.fiatBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(), availableForWithdrawal = value.availableForWithdrawal, + fiatRate = value.fiatRate, cards = value.cards.map { card -> PaymentAccountStatusValueDM.TangemPayCard( id = card.id, @@ -60,6 +61,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( ) is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty() is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount( + fiatRate = value.fiatRate, fiatBalance = value.fiatBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(), ) @@ -92,6 +94,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( cryptoBalance = value.cryptoBalance.toDomain(), availableForWithdrawal = value.availableForWithdrawal, cryptoCurrency = cryptoCurrency, + fiatRate = value.fiatRate, cards = value.cards.map { card -> TangemPayCard( id = card.id, @@ -121,6 +124,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), cryptoCurrency = cryptoCurrency, + fiatRate = value.fiatRate, ) null -> PaymentAccountStatusValue.Error.Unavailable } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 0c6f02473e..ebac88be49 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -5,9 +5,9 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter +import com.tangem.data.pay.entity.DefaultTangemPayCurrencyFactory import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* @@ -20,19 +20,12 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory +import com.tangem.domain.pay.TangemPayCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* -import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase -import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase -import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase -import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase -import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase -import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase -import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase import com.tangem.domain.pay.usecase.* import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase @@ -74,9 +67,7 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindTangemPayCryptoCurrencyFactory( - factory: DefaultTangemPayCryptoCurrencyFactory, - ): TangemPayCryptoCurrencyFactory + fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory @Binds @Singleton diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/DefaultTangemPayCurrencyFactory.kt similarity index 69% rename from data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt rename to data/visa/src/main/kotlin/com/tangem/data/pay/entity/DefaultTangemPayCurrencyFactory.kt index ede6bba797..711c82bc5f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/DefaultTangemPayCurrencyFactory.kt @@ -8,20 +8,21 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.requireUserWalletsSync import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayCurrencyFactory import javax.inject.Inject import javax.inject.Singleton @Singleton -internal class TangemPayCurrencyFactory @Inject constructor( +internal class DefaultTangemPayCurrencyFactory @Inject constructor( excludedBlockchains: ExcludedBlockchains, private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, -) { +) : TangemPayCurrencyFactory { private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { CryptoCurrencyFactory(excludedBlockchains) } - fun create(userWalletId: UserWalletId): CryptoCurrency.Token { + override fun create(userWalletId: UserWalletId): CryptoCurrency.Token { val userWallet = userWalletsListRepository.requireUserWalletsSync() .firstOrNull { it.walletId == userWalletId } ?: error("User wallet with id $userWalletId not found") @@ -32,18 +33,11 @@ internal class TangemPayCurrencyFactory @Inject constructor( ) return cryptoCurrencyFactory.createToken( network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TOKEN_ID), - name = TOKEN_NAME, - symbol = TOKEN_NAME, - contractAddress = TOKEN_CONTRACT_ADDRESS, - decimals = TOKEN_DECIMALS, + rawId = TangemPayCurrencyFactory.TOKEN_ID, + name = TangemPayCurrencyFactory.TOKEN_NAME, + symbol = TangemPayCurrencyFactory.TOKEN_NAME, + contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS, + decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS, ) } - - companion object { - internal const val TOKEN_ID = "usd-coin" - internal const val TOKEN_NAME = "USDC" - internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" - internal const val TOKEN_DECIMALS = 6 - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 81159ea6a3..bf330d9db2 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -1,7 +1,6 @@ package com.tangem.data.pay.flow import arrow.core.Either -import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource @@ -11,7 +10,9 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardLimitData +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo @@ -21,6 +22,8 @@ import com.tangem.domain.pay.model.TangemPayEntryPoint import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.security.DeviceSecurityInfoProvider @@ -30,6 +33,7 @@ import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.isActive +import java.math.BigDecimal import javax.inject.Inject import kotlin.time.Duration.Companion.minutes @@ -45,6 +49,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, private val eligibilityManager: TangemPayEligibilityManager, private val reissueCardRepository: TangemPayReissueCardRepository, + private val singleQuoteSupplier: SingleQuoteStatusSupplier, ) : PaymentAccountStatusFetcher { private val logger = TangemLogger.withTag(TAG) @@ -257,6 +262,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { + val quotesData = singleQuoteSupplier.getSyncOrNull( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID), + )?.value as? QuoteStatus.Data val cardInfo = this.cardInfo val productInstance = this.productInstance @@ -279,12 +287,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( fiatBalance = fiatBalance, cryptoBalance = cryptoBalance, cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), + fiatRate = quotesData?.fiatRate, ) } cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState( userWalletId = userWalletId, productInstance = productInstance, cardInfo = cardInfo, + fiatRate = quotesData?.fiatRate, customerId = requireNotNull(customerId) { "CustomerId must not be null" }, ) else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) @@ -296,6 +306,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( productInstance: CustomerInfo.ProductInstance, cardInfo: CustomerInfo.CardInfo, customerId: String, + fiatRate: BigDecimal?, ): PaymentAccountStatusValue { val reissueOrder = reissueCardRepository.getReissueOrderInfo( userWalletId = userWalletId, @@ -316,6 +327,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( cryptoBalance = cardInfo.cryptoBalance, availableForWithdrawal = cardInfo.availableForWithdrawal, cryptoCurrency = cryptoCurrency, + fiatRate = fiatRate, cards = listOf( TangemPayCard( id = productInstance.cardId, diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt index 689f793d48..dfceb506ec 100644 --- a/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt @@ -1,12 +1,12 @@ package com.tangem.data.pay.converter import com.google.common.truth.Truth.assertThat -import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayCurrencyFactory import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Nested @@ -70,6 +70,7 @@ internal class PaymentAccountStatusValueDMConverterTest { ), cryptoBalance = cryptoBalance(), cryptoCurrency = cryptoCurrency, + fiatRate = BigDecimal("1.05"), ) // WHEN @@ -80,6 +81,7 @@ internal class PaymentAccountStatusValueDMConverterTest { val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100")) assertThat(dm.fiatBalance.currency).isEqualTo("USD") + assertThat(dm.fiatRate).isEqualTo(BigDecimal("1.05")) } @Test @@ -144,6 +146,7 @@ internal class PaymentAccountStatusValueDMConverterTest { currency = "EUR", ), cryptoBalance = cryptoBalanceDM(), + fiatRate = BigDecimal("0.92"), ) // WHEN @@ -155,6 +158,7 @@ internal class PaymentAccountStatusValueDMConverterTest { assertThat(deactivated.source).isEqualTo(StatusSource.CACHE) assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200")) assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR") + assertThat(deactivated.fiatRate).isEqualTo(BigDecimal("0.92")) } @Test diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index 756bb4b54a..4c10c467e0 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -31,8 +31,14 @@ sealed class PaymentAccountStatusValue { is UnderReview, -> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source) is Loading -> TotalFiatBalance.Loading - is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) - is Deactivated -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + is Loaded -> { + val rate = this.fiatRate ?: return TotalFiatBalance.Failed + TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source) + } + is Deactivated -> { + val rate = this.fiatRate ?: return TotalFiatBalance.Failed + TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source) + } } /** @@ -99,6 +105,11 @@ sealed class PaymentAccountStatusValue { * * @property source The source of the status information. * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + * @property cryptoCurrency The crypto currency held by the deactivated account. + * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, + * or `null` if the quote is not yet available. When `null`, + * [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. */ @Serializable data class Deactivated( @@ -106,25 +117,15 @@ sealed class PaymentAccountStatusValue { val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, val cryptoCurrency: CryptoCurrency.Token, + val fiatRate: SerializedBigDecimal?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, - value = CryptoCurrencyStatus.Loaded( + value = buildCryptoCurrencyStatusValue( amount = cryptoBalance.balance, fiatAmount = fiatBalance.availableBalance, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - networkAddress = NetworkAddress.Single( - defaultAddress = NetworkAddress.Address( - type = NetworkAddress.Address.Type.Primary, - value = cryptoBalance.depositAddress, - ), - ), - sources = CryptoCurrencyStatus.Sources(), - pendingTransactions = emptySet(), - stakingBalance = null, - yieldSupplyStatus = null, - hasCurrentNetworkTransactions = false, + fiatRate = fiatRate, + depositAddress = cryptoBalance.depositAddress, ), ) } @@ -139,7 +140,11 @@ sealed class PaymentAccountStatusValue { * @property fiatBalance The fiat balance details. * @property cryptoBalance The crypto balance details. * @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds). + * @property cryptoCurrency The crypto currency held by the account. * @property cards The list of user's cards. + * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, + * or `null` if the quote is not yet available. When `null`, + * [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. */ @Serializable data class Loaded( @@ -152,25 +157,15 @@ sealed class PaymentAccountStatusValue { val availableForWithdrawal: SerializedBigDecimal, val cryptoCurrency: CryptoCurrency.Token, val cards: List, + val fiatRate: SerializedBigDecimal?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, - value = CryptoCurrencyStatus.Loaded( + value = buildCryptoCurrencyStatusValue( amount = availableForWithdrawal, fiatAmount = fiatBalance.availableBalance, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - networkAddress = NetworkAddress.Single( - defaultAddress = NetworkAddress.Address( - type = NetworkAddress.Address.Type.Primary, - value = cryptoBalance.depositAddress, - ), - ), - sources = CryptoCurrencyStatus.Sources(), - pendingTransactions = emptySet(), - stakingBalance = null, - yieldSupplyStatus = null, - hasCurrentNetworkTransactions = false, + fiatRate = fiatRate, + depositAddress = cryptoBalance.depositAddress, ), ) } @@ -235,6 +230,44 @@ sealed class PaymentAccountStatusValue { ) } +private fun buildCryptoCurrencyStatusValue( + amount: SerializedBigDecimal, + fiatAmount: SerializedBigDecimal, + fiatRate: SerializedBigDecimal?, + depositAddress: String, +): CryptoCurrencyStatus.Value { + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + type = NetworkAddress.Address.Type.Primary, + value = depositAddress, + ), + ) + return if (fiatRate != null) { + CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = fiatAmount, + fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, + networkAddress = networkAddress, + sources = CryptoCurrencyStatus.Sources(), + pendingTransactions = emptySet(), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + ) + } else { + CryptoCurrencyStatus.NoQuote( + amount = amount, + networkAddress = networkAddress, + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + sources = CryptoCurrencyStatus.Sources(), + ) + } +} + fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId } fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt index 2071908140..babba04024 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt @@ -51,7 +51,9 @@ class BalanceFetchingOperations( async { val result = when (source) { FetchingSource.NETWORK -> fetchNetworks(userWalletId, currencies) - FetchingSource.QUOTE -> fetchQuotes(currencies) + FetchingSource.QUOTE -> fetchQuotes( + currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + ) FetchingSource.STAKING -> fetchStaking(userWalletId, currencies) } source to result @@ -85,17 +87,14 @@ class BalanceFetchingOperations( } /** - * Fetches quotes for the given currencies. + * Fetches quotes for the given raw currency ids. * - * @param currencies the cryptocurrencies to fetch quotes for + * @param rawCurrencyIds the raw currency ids to fetch quotes for * @return Either with Unit on success or Throwable on failure */ - suspend fun fetchQuotes(currencies: Collection): Either { + suspend fun fetchQuotes(rawCurrencyIds: Set): Either { return multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), + params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrencyIds, appCurrencyId = null), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 8f43d68f33..f4345e9c39 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -13,6 +13,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.TangemPayCurrencyFactory import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory @@ -173,6 +174,7 @@ class WalletBalanceFetcher internal constructor( // Fetch TangemPay separately — may run long-polling, so it must not block balance error checking if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) { + balanceFetchingOperations.fetchQuotes(rawCurrencyIds = setOf(TangemPayCurrencyFactory.TOKEN_ID)) paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt deleted file mode 100644 index 31a2537914..0000000000 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.pay - -import arrow.core.Either -import com.tangem.core.error.UniversalError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet - -@Deprecated("TangemPayCurrencyFactory") -interface TangemPayCryptoCurrencyFactory { - - fun create(userWallet: UserWallet, chainId: Int): Either -} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCurrencyFactory.kt new file mode 100644 index 0000000000..39bdae4191 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCurrencyFactory.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Factory that builds the [CryptoCurrency.Token] used by Tangem Pay (USDC on Polygon) for a given user wallet. + * + * Replaces the deprecated `TangemPayCryptoCurrencyFactory`: callers no longer pass the chain id explicitly — + * the underlying network is resolved from the wallet. + */ +interface TangemPayCurrencyFactory { + + /** + * Builds the Tangem Pay token bound to the network of the wallet identified by [userWalletId]. + * + * @throws IllegalStateException if no wallet with [userWalletId] is currently loaded. + */ + fun create(userWalletId: UserWalletId): CryptoCurrency.Token + + /** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */ + companion object { + /** CoinGecko-style raw id used to query quotes for the Tangem Pay token. */ + val TOKEN_ID = CryptoCurrency.RawID("usd-coin") + const val TOKEN_NAME = "USDC" + const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" + const val TOKEN_DECIMALS = 6 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 7a5d2caf8c..c9f1c94895 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 @@ -10,6 +10,8 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta 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.assetsdiscovery.model.AssetsDiscoveryProgress +import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase @@ -26,19 +28,17 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress -import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.hot.sdk.model.HotWalletId import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.addIf @@ -222,10 +222,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) { val notification = when (status.value) { is PaymentAccountStatusValue.Error.NotSynced -> WalletNotification.Warning.TangemPayRefreshNeeded( - buttonText = when (userWallet) { - is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) - is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_button) - }, + buttonText = resourceReference(id = R.string.tangempay_sync_needed_button), onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, shouldShowProgress = false, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index 2bd65ca2d6..c4238c5988 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -251,10 +251,7 @@ internal class GetWalletNotificationsFactory @Inject constructor( ) { val notification = when (status.value) { is PaymentAccountStatusValue.Error.NotSynced -> WalletNotificationUM.TangemPayRefreshNeeded( - buttonText = when (userWallet) { - is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) - is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_button) - }, + buttonText = resourceReference(id = R.string.tangempay_sync_needed_button), onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, shouldShowProgress = false, ) From a3abea2e787ae51ba7c08f0d816216b5a9aad97e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 13:22:02 +0300 Subject: [PATCH 06/16] Updated on 2026-08-14 --- .../main/java/com/tangem/domain/yield/supply/FeeExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt index 07ce01ecb4..9a798bdc38 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt @@ -6,7 +6,7 @@ import java.math.BigInteger import java.math.RoundingMode private val HUNDRED_PERCENT = 100.toBigInteger() // base 100% -val INCREASE_GAS_LIMIT_FOR_SUPPLY = 120.toBigInteger() // 20% increase +val INCREASE_GAS_LIMIT_FOR_SUPPLY = 140.toBigInteger() // 20% increase fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = when (this) { is Fee.Ethereum.Legacy -> copy( From f990f3c833250d0b9afbf67abf6eddbf94f7b123 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 16:31:56 +0400 Subject: [PATCH 07/16] Updated on 2026-08-14 --- .../models/YieldBoostStatusResponse.kt | 1 - .../converter/YieldBoostStatusConverter.kt | 47 +++++--------- .../YieldBoostStatusConverterTest.kt | 46 +++++++------- .../yield/supply/models/YieldBoostStatus.kt | 26 ++++---- ...eldBoostPromoEnabledForTokenUseCaseTest.kt | 28 +-------- ...ouldShowYieldBoostMainBannerUseCaseTest.kt | 7 +-- .../impl/active/model/BoostBlockState.kt | 22 +++++++ .../active/model/YieldSupplyActiveModel.kt | 62 +++++++------------ .../impl/active/model/BoostBlockStateTest.kt | 54 ++++++++++++++++ 9 files changed, 152 insertions(+), 141 deletions(-) create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockState.kt create mode 100644 features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockStateTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/YieldBoostStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/YieldBoostStatusResponse.kt index dd697566df..37135fb787 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/YieldBoostStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/YieldBoostStatusResponse.kt @@ -11,7 +11,6 @@ data class YieldBoostStatusResponse( @Json(name = "userAddress") val userAddress: String?, @Json(name = "contractAddress") val contractAddress: String?, @Json(name = "promoEnrollmentStatus") val promoEnrollmentStatus: String, - @Json(name = "activationDate") val activationDate: String?, @Json(name = "qualificationEndDate") val qualificationEndDate: String?, @Json(name = "disqualificationReason") val disqualificationReason: String?, ) \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverter.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverter.kt index 4ddbac4301..ef54706c37 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverter.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverter.kt @@ -16,43 +16,26 @@ internal object YieldBoostStatusConverter { private const val REASON_CLOSED = "closed" fun convert(dto: YieldBoostStatusResponse): YieldBoostStatus = when (dto.promoEnrollmentStatus.lowercase()) { - STATUS_ACTIVE -> dto.toActive() ?: YieldBoostStatus.NotStarted - STATUS_COMPLETED -> dto.toCompleted() ?: YieldBoostStatus.NotStarted + STATUS_ACTIVE, STATUS_COMPLETED -> dto.toEnrolled() STATUS_DISQUALIFIED -> YieldBoostStatus.Disqualified(reason = dto.disqualificationReason.toReason()) STATUS_NOT_STARTED -> YieldBoostStatus.NotStarted else -> YieldBoostStatus.NotStarted // forward-compat: unknown status → treat as NotStarted } - /** Backend `"active"` → [YieldBoostStatus.Active]. Returns `null` if mandatory dates can't be parsed. */ - private fun YieldBoostStatusResponse.toActive(): YieldBoostStatus.Active? { - val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null - val qualificationEnd = - qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null - return YieldBoostStatus.Active( - tokenName = tokenName.orEmpty(), - networkId = networkId.orEmpty(), - moduleAddress = moduleAddress.orEmpty(), - userAddress = userAddress.orEmpty(), - contractAddress = contractAddress.orEmpty(), - activationDate = activation, - qualificationEndDate = qualificationEnd, - ) - } - - private fun YieldBoostStatusResponse.toCompleted(): YieldBoostStatus.Completed? { - val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null - val qualificationEnd = - qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null - return YieldBoostStatus.Completed( - tokenName = tokenName.orEmpty(), - networkId = networkId.orEmpty(), - moduleAddress = moduleAddress.orEmpty(), - userAddress = userAddress.orEmpty(), - contractAddress = contractAddress.orEmpty(), - activationDate = activation, - qualificationEndDate = qualificationEnd, - ) - } + /** + * Backend `"active"` / `"completed"` → [YieldBoostStatus.Enrolled]. + * + * An unparseable / missing `qualificationEndDate` is kept as `null` (block hidden) — never downgraded to + * [YieldBoostStatus.NotStarted], which would re-prompt an already-enrolled user to join. + */ + private fun YieldBoostStatusResponse.toEnrolled(): YieldBoostStatus.Enrolled = YieldBoostStatus.Enrolled( + tokenName = tokenName.orEmpty(), + networkId = networkId.orEmpty(), + moduleAddress = moduleAddress.orEmpty(), + userAddress = userAddress.orEmpty(), + contractAddress = contractAddress.orEmpty(), + qualificationEndDate = qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() }, + ) private fun String?.toReason(): YieldBoostStatus.Disqualified.Reason = when (this?.lowercase()) { REASON_FROD -> YieldBoostStatus.Disqualified.Reason.FROD diff --git a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverterTest.kt b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverterTest.kt index 3e9c7c3850..0741d4ae6c 100644 --- a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverterTest.kt +++ b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/promo/converter/YieldBoostStatusConverterTest.kt @@ -3,11 +3,11 @@ package com.tangem.data.yield.supply.promo.converter import com.google.common.truth.Truth.assertThat import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse import com.tangem.domain.yield.supply.models.YieldBoostStatus +import kotlinx.datetime.Instant import org.junit.jupiter.api.Test class YieldBoostStatusConverterTest { - private val activation = "2026-05-01T00:00:00Z" private val qualificationEnd = "2026-06-01T00:00:00Z" @Test @@ -20,7 +20,7 @@ class YieldBoostStatusConverterTest { } @Test - fun `GIVEN active backend status with valid dates WHEN convert THEN returns Active`() { + fun `GIVEN active backend status with valid date WHEN convert THEN returns Enrolled`() { val dto = dto( promoEnrollmentStatus = "active", tokenName = "USD Coin", @@ -28,47 +28,49 @@ class YieldBoostStatusConverterTest { moduleAddress = "0xmodule", userAddress = "0xuser", contractAddress = "0xcontract", - activationDate = activation, qualificationEndDate = qualificationEnd, ) val result = YieldBoostStatusConverter.convert(dto) - assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java) - val active = result as YieldBoostStatus.Active - assertThat(active.tokenName).isEqualTo("USD Coin") - assertThat(active.networkId).isEqualTo("ethereum") - assertThat(active.contractAddress).isEqualTo("0xcontract") + assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java) + val enrolled = result as YieldBoostStatus.Enrolled + assertThat(enrolled.tokenName).isEqualTo("USD Coin") + assertThat(enrolled.networkId).isEqualTo("ethereum") + assertThat(enrolled.contractAddress).isEqualTo("0xcontract") + assertThat(enrolled.qualificationEndDate).isEqualTo(Instant.parse(qualificationEnd)) } @Test - fun `GIVEN active status missing activationDate WHEN convert THEN falls back to NotStarted`() { + fun `GIVEN active status missing qualificationEndDate WHEN convert THEN returns Enrolled with null date`() { val dto = dto( promoEnrollmentStatus = "active", - activationDate = null, - qualificationEndDate = qualificationEnd, + contractAddress = "0xcontract", + qualificationEndDate = null, ) val result = YieldBoostStatusConverter.convert(dto) - assertThat(result).isEqualTo(YieldBoostStatus.NotStarted) + assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java) + assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull() } @Test - fun `GIVEN active status with malformed activationDate WHEN convert THEN falls back to NotStarted`() { + fun `GIVEN active status with malformed qualificationEndDate WHEN convert THEN returns Enrolled with null date`() { val dto = dto( promoEnrollmentStatus = "active", - activationDate = "not-an-iso", - qualificationEndDate = qualificationEnd, + contractAddress = "0xcontract", + qualificationEndDate = "not-an-iso", ) val result = YieldBoostStatusConverter.convert(dto) - assertThat(result).isEqualTo(YieldBoostStatus.NotStarted) + assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java) + assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull() } @Test - fun `GIVEN completed status with valid dates WHEN convert THEN returns Completed`() { + fun `GIVEN completed status with valid date WHEN convert THEN returns Enrolled`() { val dto = dto( promoEnrollmentStatus = "completed", tokenName = "USDT", @@ -76,13 +78,14 @@ class YieldBoostStatusConverterTest { moduleAddress = "0xmodule", userAddress = "0xuser", contractAddress = "0xcontract", - activationDate = "2026-04-01T00:00:00Z", qualificationEndDate = "2026-05-01T00:00:00Z", ) val result = YieldBoostStatusConverter.convert(dto) - assertThat(result).isInstanceOf(YieldBoostStatus.Completed::class.java) + assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java) + assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate) + .isEqualTo(Instant.parse("2026-05-01T00:00:00Z")) } @Test @@ -153,13 +156,12 @@ class YieldBoostStatusConverterTest { moduleAddress = "0xmodule", userAddress = "0xuser", contractAddress = "0xcontract", - activationDate = activation, qualificationEndDate = qualificationEnd, ) val result = YieldBoostStatusConverter.convert(dto) - assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java) + assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java) } private fun dto( @@ -169,7 +171,6 @@ class YieldBoostStatusConverterTest { moduleAddress: String? = null, userAddress: String? = null, contractAddress: String? = null, - activationDate: String? = null, qualificationEndDate: String? = null, disqualificationReason: String? = null, ) = YieldBoostStatusResponse( @@ -179,7 +180,6 @@ class YieldBoostStatusConverterTest { userAddress = userAddress, contractAddress = contractAddress, promoEnrollmentStatus = promoEnrollmentStatus, - activationDate = activationDate, qualificationEndDate = qualificationEndDate, disqualificationReason = disqualificationReason, ) diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldBoostStatus.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldBoostStatus.kt index 383e27cef2..640702d5cf 100644 --- a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldBoostStatus.kt +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldBoostStatus.kt @@ -6,26 +6,22 @@ sealed interface YieldBoostStatus { data object NotStarted : YieldBoostStatus - /** User entered boost, qualification period is still running. */ - data class Active( + /** + * User is enrolled in the boost (backend `active` or `completed`). + * + * The boost block on the active screen is driven entirely by [qualificationEndDate], which the backend + * computes as the end of the bonus-accrual period: + * - `null` — nothing is shown; + * - in the future — days left until the date; + * - reached / passed — awaiting payout. + */ + data class Enrolled( val tokenName: String, val networkId: String, val moduleAddress: String, val userAddress: String, val contractAddress: String, - val activationDate: Instant, - val qualificationEndDate: Instant, - ) : YieldBoostStatus - - /** Boost has finished (backend `completed`). */ - data class Completed( - val tokenName: String, - val networkId: String, - val moduleAddress: String, - val userAddress: String, - val contractAddress: String, - val activationDate: Instant, - val qualificationEndDate: Instant, + val qualificationEndDate: Instant?, ) : YieldBoostStatus data class Disqualified(val reason: Reason) : YieldBoostStatus { diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt index 9ba69c2fea..9afbbb6492 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt @@ -91,21 +91,10 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest { } @Test - fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest { + fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest { val token = createToken() coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo() - coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus() - - val result = useCase(userWalletId, token) - - assertThat(result.getOrNull()).isFalse() - } - - @Test - fun `GIVEN status is Completed WHEN invoke THEN returns Right(false)`() = runTest { - val token = createToken() - coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo() - coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns completedStatus() + coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus() val result = useCase(userWalletId, token) @@ -162,26 +151,15 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest { link = null, ) - private fun activeStatus() = YieldBoostStatus.Active( + private fun enrolledStatus() = YieldBoostStatus.Enrolled( tokenName = "USD Coin", networkId = networkRawId, moduleAddress = "0xmodule", userAddress = "0xuser", contractAddress = contractAddress, - activationDate = Instant.parse("2026-05-01T00:00:00Z"), qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"), ) - private fun completedStatus() = YieldBoostStatus.Completed( - tokenName = "USD Coin", - networkId = networkRawId, - moduleAddress = "0xmodule", - userAddress = "0xuser", - contractAddress = contractAddress, - activationDate = Instant.parse("2026-04-01T00:00:00Z"), - qualificationEndDate = Instant.parse("2026-05-01T00:00:00Z"), - ) - private fun createToken( contractAddress: String = this.contractAddress, networkRawId: String = this.networkRawId, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt index 9461882859..ae20808d28 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt @@ -57,9 +57,9 @@ class ShouldShowYieldBoostMainBannerUseCaseTest { } @Test - fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest { + fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest { coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo() - coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus() + coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus() val result = useCase(userWalletId) @@ -92,13 +92,12 @@ class ShouldShowYieldBoostMainBannerUseCaseTest { link = null, ) - private fun activeStatus() = YieldBoostStatus.Active( + private fun enrolledStatus() = YieldBoostStatus.Enrolled( tokenName = "USD Coin", networkId = networkRawId, moduleAddress = "0xmodule", userAddress = "0xuser", contractAddress = contractAddress, - activationDate = Instant.parse("2026-05-01T00:00:00Z"), qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"), ) } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockState.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockState.kt new file mode 100644 index 0000000000..1eda6da853 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockState.kt @@ -0,0 +1,22 @@ +package com.tangem.features.yield.supply.impl.active.model + +import kotlinx.datetime.Instant + +/** What the boost block on the active screen should display, derived solely from the qualification end date. */ +internal sealed interface BoostBlockState { + + /** Qualification period is still running — show the countdown. */ + data class DaysLeft(val days: Int) : BoostBlockState + + /** Qualification period is over — show the awaiting-payout copy. */ + data object AwaitingPayout : BoostBlockState + + /** No qualification end date — show nothing. */ + data object Hidden : BoostBlockState +} + +internal fun resolveBoostBlockState(qualificationEndDate: Instant?, now: Instant): BoostBlockState = when { + qualificationEndDate == null -> BoostBlockState.Hidden + now >= qualificationEndDate -> BoostBlockState.AwaitingPayout + else -> BoostBlockState.DaysLeft(days = (qualificationEndDate - now).inWholeDays.toInt()) +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index 9afb505aef..2bc86a016d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -54,8 +54,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock import javax.inject.Inject -import kotlin.math.max -import kotlin.time.Duration.Companion.milliseconds @Suppress("LongParameterList", "LargeClass") @ModelScoped @@ -245,21 +243,18 @@ internal class YieldSupplyActiveModel @Inject constructor( modelScope.launch(dispatchers.io) { val status = getYieldBoostStatusUseCase(userWalletId).getOrNull() ?: return@launch val token = cryptoCurrency as? CryptoCurrency.Token ?: return@launch - when { - status is YieldBoostStatus.Active && status.matches(token) -> { - uiState.update { - it.copy(boostText = buildActiveBoostText(status), onBoostClick = ::onBoostClick) - } - } - status is YieldBoostStatus.Completed && status.matches(token) -> { - uiState.update { - it.copy( - boostText = resourceReference(CoreResR.string.yield_promo_completed), - onBoostClick = ::onBoostClick, - ) - } - } + if (status !is YieldBoostStatus.Enrolled || !status.matches(token)) return@launch + + val state = resolveBoostBlockState( + qualificationEndDate = status.qualificationEndDate, + now = Clock.System.now(), + ) + val boostText = when (state) { + is BoostBlockState.DaysLeft -> buildDaysLeftText(state.days) + BoostBlockState.AwaitingPayout -> resourceReference(CoreResR.string.yield_promo_completed) + BoostBlockState.Hidden -> return@launch } + uiState.update { it.copy(boostText = boostText, onBoostClick = ::onBoostClick) } } } @@ -274,32 +269,17 @@ internal class YieldSupplyActiveModel @Inject constructor( ) } - private fun buildActiveBoostText(status: YieldBoostStatus.Active): TextReference { - val daysLeft = computeDaysLeft(status.qualificationEndDate.toEpochMilliseconds()) - return combinedReference( - pluralReference( - id = CoreResR.plurals.common_days, - count = daysLeft, - formatArgs = wrappedList(daysLeft), - ), - stringReference(" "), - resourceReference(CoreResR.string.yield_promo_left_title), - ) - } + private fun buildDaysLeftText(daysLeft: Int): TextReference = combinedReference( + pluralReference( + id = CoreResR.plurals.common_days, + count = daysLeft, + formatArgs = wrappedList(daysLeft), + ), + stringReference(" "), + resourceReference(CoreResR.string.yield_promo_left_title), + ) - private fun computeDaysLeft(qualificationEndEpochMillis: Long): Int { - val nowMillis = Clock.System.now().toEpochMilliseconds() - val deltaMillis = max(qualificationEndEpochMillis - nowMillis, 0L) - return deltaMillis.milliseconds.inWholeDays.toInt() - } - - private fun YieldBoostStatus.Active.matches(token: CryptoCurrency.Token): Boolean = - matchesToken(contractAddress = contractAddress, networkId = networkId, token = token) - - private fun YieldBoostStatus.Completed.matches(token: CryptoCurrency.Token): Boolean = - matchesToken(contractAddress = contractAddress, networkId = networkId, token = token) - - private fun matchesToken(contractAddress: String, networkId: String, token: CryptoCurrency.Token): Boolean { + private fun YieldBoostStatus.Enrolled.matches(token: CryptoCurrency.Token): Boolean { val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) return contractAddress.equals(token.contractAddress, ignoreCase = shouldIgnoreCase) && networkId == token.network.rawId diff --git a/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockStateTest.kt b/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockStateTest.kt new file mode 100644 index 0000000000..9d2dd5f854 --- /dev/null +++ b/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/active/model/BoostBlockStateTest.kt @@ -0,0 +1,54 @@ +package com.tangem.features.yield.supply.impl.active.model + +import com.google.common.truth.Truth.assertThat +import kotlinx.datetime.Instant +import org.junit.jupiter.api.Test + +internal class BoostBlockStateTest { + + private val now = Instant.parse("2026-05-28T00:00:00Z") + + @Test + fun `GIVEN null qualificationEndDate WHEN resolve THEN Hidden`() { + val result = resolveBoostBlockState(qualificationEndDate = null, now = now) + + assertThat(result).isEqualTo(BoostBlockState.Hidden) + } + + @Test + fun `GIVEN future qualificationEndDate WHEN resolve THEN DaysLeft with whole days`() { + val result = resolveBoostBlockState( + qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"), + now = now, + ) + + assertThat(result).isEqualTo(BoostBlockState.DaysLeft(days = 4)) + } + + @Test + fun `GIVEN qualificationEndDate less than a day away WHEN resolve THEN DaysLeft zero`() { + val result = resolveBoostBlockState( + qualificationEndDate = Instant.parse("2026-05-28T18:00:00Z"), + now = now, + ) + + assertThat(result).isEqualTo(BoostBlockState.DaysLeft(days = 0)) + } + + @Test + fun `GIVEN qualificationEndDate equal to now WHEN resolve THEN AwaitingPayout`() { + val result = resolveBoostBlockState(qualificationEndDate = now, now = now) + + assertThat(result).isEqualTo(BoostBlockState.AwaitingPayout) + } + + @Test + fun `GIVEN past qualificationEndDate WHEN resolve THEN AwaitingPayout`() { + val result = resolveBoostBlockState( + qualificationEndDate = Instant.parse("2026-05-01T00:00:00Z"), + now = now, + ) + + assertThat(result).isEqualTo(BoostBlockState.AwaitingPayout) + } +} \ No newline at end of file From 0e264321b03d8317adc06542985e193ad3952b38 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 16:32:23 +0400 Subject: [PATCH 08/16] Updated on 2026-08-14 --- .../feature/swap/ui/ProviderItemSimple.kt | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt index 71cd53b67e..23d15675f1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt @@ -3,8 +3,10 @@ package com.tangem.feature.swap.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -31,6 +34,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SendConfirmScreenTestTags import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState @@ -74,19 +78,28 @@ internal fun ProviderItemBlockSimple(state: ProviderState, modifier: Modifier = private fun SimpleProviderTrailing(state: ProviderState) { when (state) { is ProviderState.Content -> { - SubcomposeAsyncImage( - model = ImageRequest.Builder(context = LocalContext.current) - .data(state.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), - loading = { RectangleShimmer(radius = 4.dp) }, - error = { RectangleShimmer(radius = 4.dp) }, - contentDescription = null, - modifier = Modifier - .size(TangemTheme.dimens.size20) - .clip(RoundedCornerShape(TangemTheme.dimens.radius4)), - ) + Box { + SubcomposeAsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = 4.dp) }, + error = { RectangleShimmer(radius = 4.dp) }, + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)), + ) + if (state.additionalBadge is ProviderState.AdditionalBadge.BestTrade) { + SimpleBestRateBadge( + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = 5.dp, y = 6.dp), + ) + } + } Text( text = state.name, style = TangemTheme.typography.body2, @@ -118,6 +131,26 @@ private fun SimpleProviderTrailing(state: ProviderState) { } } +@Composable +private fun SimpleBestRateBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background(TangemTheme.colors.stroke.transparency, RoundedCornerShape(120.dp)) + .padding(1.5.dp) + .background(TangemTheme.colors.icon.accent, RoundedCornerShape(120.dp)), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rounded_star_24), + tint = TangemTheme.colors.icon.constant, + contentDescription = null, + modifier = Modifier + .padding(horizontal = 2.dp, vertical = 2.dp) + .size(8.dp) + .testTag(SendConfirmScreenTestTags.BEST_RATE_BADGE), + ) + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -142,6 +175,18 @@ private class SimpleProviderPreview : PreviewParameterProvider { namePrefix = ProviderState.PrefixType.NONE, onProviderClick = {}, ), + ProviderState.Content( + id = "3", + name = "Changelly", + type = "CEX", + iconUrl = "", + subtitle = stringReference("1 SOL ≈ 0.0011337 BTC"), + selectionType = ProviderState.SelectionType.CLICK, + additionalBadge = ProviderState.AdditionalBadge.BestTrade, + percentLowerThenBest = PercentDifference.Empty, + namePrefix = ProviderState.PrefixType.NONE, + onProviderClick = {}, + ), ProviderState.Loading(), ProviderState.Unavailable( id = "2", From 2b000c1c728ef24b8649029d3c1c1c6c9e1ea0cb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 21:38:04 +0500 Subject: [PATCH 09/16] Updated on 2026-08-14 --- .../wallet/domain/Wallet2CobrandImage.kt | 12 ++++++++++++ .../main/res/drawable/ill_adi_card2_120_106.webp | Bin 0 -> 4658 bytes .../main/res/drawable/ill_adi_card3_120_106.webp | Bin 0 -> 4528 bytes .../drawable/ill_stronghold_card2_120_106.webp | Bin 0 -> 5360 bytes .../drawable/ill_stronghold_card3_120_106.webp | Bin 0 -> 5458 bytes 5 files changed, 12 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_adi_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_adi_card3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_stronghold_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_stronghold_card3_120_106.webp diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index d802da3a1a..c6a3e8632a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -410,4 +410,16 @@ internal enum class Wallet2CobrandImage( cards3ResId = R.drawable.ill_metaplanet_card3_120_106, batchIds = setOf("BB000040"), ), + + Adi( + cards2ResId = R.drawable.ill_adi_card2_120_106, + cards3ResId = R.drawable.ill_adi_card3_120_106, + batchIds = setOf("BB000053"), + ), + + Stronghold( + cards2ResId = R.drawable.ill_stronghold_card2_120_106, + cards3ResId = R.drawable.ill_stronghold_card3_120_106, + batchIds = setOf("BB000054"), + ), } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_adi_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_adi_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..c913954cfb2ddeb05b300216f1afec6dff0d9691 GIT binary patch literal 4658 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_adi_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_adi_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..43a172cb3f6ae965f11d099b496ca577648456e9 GIT binary patch literal 4528 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 Oc8+DhM$<4n2 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_stronghold_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stronghold_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2ca2783c38db6a136fab45d9ca2bbdcd7025f16 GIT binary patch literal 5360 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 J3X}@~_#atYfYks1 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_stronghold_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stronghold_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..9f890bb5b0b39bc56bbda165a0a466a13c34701e GIT binary patch literal 5458 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 Date: Mon, 1 Jun 2026 13:07:10 +0500 Subject: [PATCH 10/16] Updated on 2026-08-14 --- .../DefaultTokenDetailsDeepLinkHandler.kt | 22 +++ .../DefaultTokenDetailsDeepLinkHandlerTest.kt | 150 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index e9ee88d2bd..42605a81a8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -9,6 +9,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.currency.CryptoCurrency @@ -47,6 +48,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, private val singleAccountListSupplier: SingleAccountListSupplier, + private val singleAccountListFetcher: SingleAccountListFetcher, ) : TokenDetailsDeepLinkHandler { init { @@ -81,6 +83,9 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } } + // Refresh the portfolio before searching so a token just added on the backend is present locally. + refreshAccountsIfNeeded(userWallet) + val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId) if (cryptoCurrency == null) { @@ -91,6 +96,8 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( |- $TOKEN_ID_KEY: $tokenId """.trimIndent(), ) + // Token is not in the response (not indexed yet / backend error): go to main, do not add. + appRouter.popTo(AppRoute.Wallet) return@launch } @@ -123,6 +130,21 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } } + /** + * Refreshes wallet accounts so a token just added on the backend appears in the local portfolio. + * + * Only when the app was open on push tap ([isFromOnNewIntent]) and the wallet is multi-currency: + * on cold start the fresh list is already loaded by the regular auth flow, and single-currency + * wallets have a fixed token. The fetch is best-effort — on failure we fall through and try the + * current cache, so existing tokens (e.g. swap/onramp pushes) still open without regression. + */ + private suspend fun refreshAccountsIfNeeded(userWallet: UserWallet) { + if (isFromOnNewIntent && userWallet.isMultiCurrency) { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId = userWallet.walletId)) + .onLeft { TangemLogger.e("Error on refreshing wallet accounts", it) } + } + } + private suspend fun fetchCurrency(userWallet: UserWallet, cryptoCurrency: CryptoCurrency) { val isMultiCurrency = userWallet.isMultiCurrency when { diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt index 079ec97e5a..deefc4ebdf 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt @@ -10,6 +10,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -50,6 +51,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest { private val getUserWalletUseCase: GetUserWalletUseCase = mockk() private val walletBalanceFetcher: WalletBalanceFetcher = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() + private val singleAccountListFetcher: SingleAccountListFetcher = mockk() @BeforeEach fun setUp() { @@ -57,6 +59,8 @@ class DefaultTokenDetailsDeepLinkHandlerTest { mockkObject(TangemLogger) every { analyticsEventHandler.send(any()) } just Runs every { appRouter.push(any(), any()) } just Runs + every { appRouter.popTo(route = any(), onComplete = any()) } just Runs + coEvery { singleAccountListFetcher.invoke(any()) } returns Either.Right(Unit) val userWallet: UserWallet = mockk() every { userWallet.walletId } returns mockk() every { getSelectedWalletSync() } returns Either.Right( @@ -461,6 +465,151 @@ class DefaultTokenDetailsDeepLinkHandlerTest { } } + @Test + fun `GIVEN multicurrency wallet AND isFromOnNewIntent WHEN handle deeplink THEN refresh wallet accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { + cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = cryptoCurrency) + } just Runs + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + coVerify { singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) } + } + + @Test + fun `GIVEN multicurrency wallet AND NOT isFromOnNewIntent WHEN handle deeplink THEN do not refresh accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = false) + advanceUntilIdle() + + coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) } + } + + @Test + fun `GIVEN single currency wallet AND isFromOnNewIntent WHEN handle deeplink THEN do not refresh accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockSingleCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs + coEvery { + walletBalanceFetcher.invoke(WalletBalanceFetcher.Params(userWalletId = userWalletId)) + } returns mockk() + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) } + } + + @Test + fun `GIVEN crypto not found WHEN handle deeplink THEN redirect to main`() = runTest { + val userWalletId = UserWalletId("011") + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns null + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + verify { appRouter.popTo(route = AppRoute.Wallet, onComplete = any()) } + } + + @Test + fun `GIVEN refresh failed AND token in cache WHEN handle deeplink THEN push new route`() = runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { + singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) + } returns Either.Left(IllegalStateException("service unavailable")) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { + cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = cryptoCurrency) + } just Runs + val expectedRoute = AppRoute.CurrencyDetails(userWalletId = userWalletId, currency = cryptoCurrency) + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + verify { + appRouter.push(route = expectedRoute, onComplete = any()) + } + } + + private fun defaultQueryParams() = mapOf( + WALLET_ID_KEY to "011", + NETWORK_ID_KEY to "123", + TOKEN_ID_KEY to "321", + DERIVATION_PATH_KEY to "777", + ) + + private fun mockCryptoCurrency() = mockk { + every { network } returns mockk { + every { rawId } returns "123" + every { derivationPath } returns Network.DerivationPath.Card(value = "777") + } + every { id } returns CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(rawId = "321", derivationPath = "777"), + suffix = CryptoCurrency.ID.Suffix.RawID("321"), + ) + } + + private fun mockMultiCurrencyWallet(userWalletId: UserWalletId) { + every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { + every { isMultiCurrency } returns true + every { walletId } returns userWalletId + every { isLocked } returns false + }, + ) + } + + private fun mockSingleCurrencyWallet(userWalletId: UserWalletId) { + every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { + every { isMultiCurrency } returns false + every { walletId } returns userWalletId + every { isLocked } returns false + }, + ) + } + + private fun mockSelectWallet(userWalletId: UserWalletId) { + coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { every { walletId } returns userWalletId }, + ) + } + private fun createHandler( scope: CoroutineScope, queryParams: Map, @@ -479,6 +628,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest { getUserWalletUseCase = getUserWalletUseCase, walletBalanceFetcher = walletBalanceFetcher, singleAccountListSupplier = singleAccountListSupplier, + singleAccountListFetcher = singleAccountListFetcher, getSelectedWalletSyncUseCase = getSelectedWalletSync, ) } From 83446489ba9d94f4b2cb736156859c168fffcd56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 14:44:48 +0400 Subject: [PATCH 11/16] Updated on 2026-08-14 --- .../feature/swap/models/SwapStateHolder.kt | 5 +- .../tangem/feature/swap/ui/StateBuilder.kt | 46 +++++++- .../feature/swap/ui/SwapScreenContent.kt | 71 +++++------- .../feature/swap/StateBuilderPairsTest.kt | 105 ++++++++++++++++++ 4 files changed, 179 insertions(+), 48 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index ff8dfd3624..24b806a74b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -6,9 +6,9 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.swap.models.PredefinedPercentAmount import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.ProviderState @@ -30,10 +30,10 @@ internal data class SwapStateHolder( val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, val shouldShowMaxAmount: Boolean, + val predefinedButtons: ImmutableList = persistentListOf(), val tosState: TosState? = null, val swapUIMode: SwapUIMode = SwapUIMode.Detailed, val shouldShowAbMenu: Boolean = false, - val isPredefinedButtonsEnabled: Boolean = false, val transferFooter: TextReference? = null, @@ -43,7 +43,6 @@ internal data class SwapStateHolder( val onSelectTokenClick: ((TokenSelectionDirection) -> Unit), val onSuccess: (() -> Unit), val onMaxAmountSelected: (() -> Unit)? = null, - val onPredefinedPercentSelected: ((PredefinedPercentAmount) -> Unit)? = null, val onShowPermissionBottomSheet: () -> Unit = {}, val onSwapUIModeChange: (SwapUIMode) -> Unit = {}, val onSwapTypeMenuOpened: () -> Unit = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7236b2fa02..7991737bec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -12,6 +12,7 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat @@ -26,6 +27,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet +import com.tangem.domain.swap.models.PredefinedPercentAmount import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork @@ -97,7 +99,6 @@ internal class StateBuilder( onBackClicked = actions.onBackClicked, onChangeCardsClicked = actions.onChangeCardsClicked, onMaxAmountSelected = actions.onMaxAmountSelected, - onPredefinedPercentSelected = actions.onPredefinedPercentSelected, changeCardsButtonState = ChangeCardsButtonState.DISABLED, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, onSelectTokenClick = actions.onSelectTokenClick, @@ -110,7 +111,6 @@ internal class StateBuilder( onSwapUIModeChange = actions.onSwapUIModeChange, onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened, shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled, - isPredefinedButtonsEnabled = swapFeatureToggles.isSwapPredefinedButtonsEnabled, ) } @@ -142,6 +142,10 @@ internal class StateBuilder( onClick = { }, ), shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus?.currency, toSwapCurrencyStatus?.currency), + predefinedButtons = createPredefinedButtons( + fromSwapCurrencyStatus?.currency, + toSwapCurrencyStatus?.currency, + ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, providerState = ProviderState.Empty(), priceImpact = PriceImpact.Empty, @@ -215,6 +219,7 @@ internal class StateBuilder( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, priceImpact = PriceImpact.Empty, shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency), + predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency), transferFooter = null, ) } @@ -250,6 +255,10 @@ internal class StateBuilder( onClick = { }, ), shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus?.currency, toSwapCurrencyStatus?.currency), + predefinedButtons = createPredefinedButtons( + fromSwapCurrencyStatus?.currency, + toSwapCurrencyStatus?.currency, + ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, providerState = ProviderState.Empty(), priceImpact = PriceImpact.Empty, @@ -446,6 +455,7 @@ internal class StateBuilder( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, priceImpact = PriceImpact.Empty, shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency), + predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency), ) } @@ -576,6 +586,7 @@ internal class StateBuilder( priceImpact = priceImpact, tosState = createTosState(swapProvider), shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency), + predefinedButtons = createPredefinedButtons(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency), ) } @@ -611,6 +622,37 @@ internal class StateBuilder( return !(fromToken is CryptoCurrency.Coin && fromToken.network.id == toCurrency?.network?.id) } + /** + * Builds the predefined percent buttons once per state update (off the composition path). + * The row is gated by the feature toggle; the MAX button is included only when + * [shouldShowMaxAmount] is `true` (e.g. it is dropped for a native coin swapped within the same + * network, where spending the full balance would leave nothing for the network fee). + */ + private fun createPredefinedButtons( + fromToken: CryptoCurrency?, + toCurrency: CryptoCurrency?, + ): ImmutableList { + if (!swapFeatureToggles.isSwapPredefinedButtonsEnabled) return persistentListOf() + val shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrency) + return PredefinedPercentAmount.entries + .filter { it != PredefinedPercentAmount.MAX || shouldShowMaxAmount } + .map { percent -> + PredefinedPercentButtonUM( + id = percent.name, + label = percent.toLabel(), + onClick = { actions.onPredefinedPercentSelected(percent) }, + ) + } + .toImmutableList() + } + + private fun PredefinedPercentAmount.toLabel(): TextReference = when (this) { + PredefinedPercentAmount.PERCENT_25 -> stringReference("25%") + PredefinedPercentAmount.PERCENT_50 -> stringReference("50%") + PredefinedPercentAmount.PERCENT_75 -> stringReference("75%") + PredefinedPercentAmount.MAX -> resourceReference(R.string.send_max_amount) + } + private fun createTosState(swapProvider: SwapProvider): TosState { return TosState( tosLink = swapProvider.termsOfUse?.let { termsUrl -> diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 2c85020628..bd03ee7e95 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -33,17 +33,14 @@ import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonUM import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonsRow import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SwapTokenScreenTestTags -import com.tangem.domain.swap.models.PredefinedPercentAmount import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* @@ -53,7 +50,6 @@ import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.ui.preview.SwapTransactionCardPreview.receiveCard import com.tangem.feature.swap.ui.preview.SwapTransactionCardPreview.sendCard import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Suppress("LongMethod") @Composable @@ -115,50 +111,39 @@ internal fun SwapScreenContent( MainButton(state = state) } - if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) { - val onPercentClick = state.onPredefinedPercentSelected - if (state.isPredefinedButtonsEnabled && onPercentClick != null) { - PredefinedPercentButtonsRow( - items = PredefinedPercentAmount.entries.map { percent -> - PredefinedPercentButtonUM( - id = percent.name, - label = percent.toLabel(), - onClick = { onPercentClick(percent) }, - ) - }.toImmutableList(), - modifier = Modifier - .align(Alignment.BottomCenter) - .imePadding(), - ) - } else { - Text( - text = stringResourceSafe(id = R.string.send_max_amount_label), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .align(Alignment.BottomCenter) - .imePadding() - .fillMaxWidth() - .background(TangemTheme.colors.button.secondary) - .clickable { state.onMaxAmountSelected?.invoke() } - .padding( - horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing16, - ), - textAlign = TextAlign.Start, - ) + if (keyboard is Keyboard.Opened) { + when { + state.predefinedButtons.isNotEmpty() -> { + PredefinedPercentButtonsRow( + items = state.predefinedButtons, + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding(), + ) + } + state.shouldShowMaxAmount -> { + Text( + text = stringResourceSafe(id = R.string.send_max_amount_label), + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding() + .fillMaxWidth() + .background(TangemTheme.colors.button.secondary) + .clickable { state.onMaxAmountSelected?.invoke() } + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing16, + ), + textAlign = TextAlign.Start, + ) + } } } } } -private fun PredefinedPercentAmount.toLabel() = when (this) { - PredefinedPercentAmount.PERCENT_25 -> stringReference("25%") - PredefinedPercentAmount.PERCENT_50 -> stringReference("50%") - PredefinedPercentAmount.PERCENT_75 -> stringReference("75%") - PredefinedPercentAmount.MAX -> resourceReference(R.string.send_max_amount) -} - @Composable private fun MainInfo(state: SwapStateHolder) { ConstraintLayout( diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt index ba6ee3e353..ca87599216 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt @@ -4,6 +4,12 @@ import com.google.common.truth.Truth.assertThat import com.tangem.common.routing.AppRouter import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.swap.models.PredefinedPercentAmount +import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork @@ -381,4 +387,103 @@ internal class StateBuilderPairsTest { toSwapCurrencyStatus = toStatus, ) } + + // region predefined buttons visibility + + @Nested + inner class PredefinedButtonsVisibility { + + @Test + fun `GIVEN toggle on and native coin within same network WHEN updateCurrenciesState THEN MAX button is dropped but percents stay`() { + every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns true + val baseState = buildReadyState(coldWallet) + val networkId: Network.ID = mockk(relaxed = true) + val fromStatus = buildCoinSwapCurrencyStatus(coldWallet, networkId) + val toStatus = buildCoinSwapCurrencyStatus(coldWallet, networkId) + + val result = sut.updateCurrenciesState( + uiStateHolder = baseState, + emptyAmountState = emptyAmountState, + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + shouldResetAmount = false, + ) + + // Legacy MAX text stays gated by shouldShowMaxAmount ([REDACTED_TASK_KEY] behavior preserved)... + assertThat(result.shouldShowMaxAmount).isFalse() + // ...and MAX is also dropped from the predefined row, but the percents remain. + assertThat(result.predefinedButtons.map { it.id }).containsExactly( + PredefinedPercentAmount.PERCENT_25.name, + PredefinedPercentAmount.PERCENT_50.name, + PredefinedPercentAmount.PERCENT_75.name, + ).inOrder() + } + + @Test + fun `GIVEN toggle on and non-coin WHEN updateCurrenciesState THEN all percents including MAX are built`() { + every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns true + val baseState = buildReadyState(coldWallet) + val fromStatus = buildSwapCurrencyStatus(coldWallet) + val toStatus = buildSwapCurrencyStatus(coldWallet) + + val result = sut.updateCurrenciesState( + uiStateHolder = baseState, + emptyAmountState = emptyAmountState, + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + shouldResetAmount = false, + ) + + assertThat(result.shouldShowMaxAmount).isTrue() + assertThat(result.predefinedButtons.map { it.id }) + .containsExactlyElementsIn(PredefinedPercentAmount.entries.map { it.name }) + .inOrder() + } + + @Test + fun `GIVEN toggle off WHEN updateCurrenciesState THEN no predefined buttons are built`() { + every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns false + val baseState = buildReadyState(coldWallet) + val fromStatus = buildSwapCurrencyStatus(coldWallet) + val toStatus = buildSwapCurrencyStatus(coldWallet) + + val result = sut.updateCurrenciesState( + uiStateHolder = baseState, + emptyAmountState = emptyAmountState, + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + shouldResetAmount = false, + ) + + assertThat(result.predefinedButtons).isEmpty() + } + + @Test + fun `WHEN createInitialLoadingState THEN no predefined buttons are built`() { + val result = sut.createInitialLoadingState() + + assertThat(result.predefinedButtons).isEmpty() + } + } + + // endregion + + private fun buildCoinSwapCurrencyStatus(userWallet: UserWallet, networkId: Network.ID): SwapCurrencyStatus { + val account = Account.CryptoPortfolio.createMainAccount(userWallet.walletId) + val coin: CryptoCurrency.Coin = mockk(relaxed = true) { + every { decimals } returns 18 + every { symbol } returns "ETH" + every { network } returns mockk(relaxed = true) { + every { id } returns networkId + } + } + val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) { + every { amount } returns java.math.BigDecimal("1.0") + } + return SwapCurrencyStatus( + userWallet = userWallet, + status = CryptoCurrencyStatus(currency = coin, value = statusValue), + account = account, + ) + } } \ No newline at end of file From 8c2e30f9bfa623f147fe1f316a00bfbd1315bfaa Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 18:09:03 +0400 Subject: [PATCH 12/16] Updated on 2026-08-14 --- data/dynamic-addresses/build.gradle.kts | 1 + .../DynamicAddressesInitializer.kt | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/data/dynamic-addresses/build.gradle.kts b/data/dynamic-addresses/build.gradle.kts index 0c1a34ea9b..7d242b8d34 100644 --- a/data/dynamic-addresses/build.gradle.kts +++ b/data/dynamic-addresses/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { // region Project - Domain implementation(projects.domain.account) + implementation(projects.domain.common) implementation(projects.domain.dynamicAddresses) implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.models) diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt index ec96dcdcea..903a2b4575 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt @@ -1,5 +1,7 @@ package com.tangem.data.dynamicaddresses +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase @@ -7,6 +9,7 @@ import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject @@ -21,11 +24,22 @@ class DynamicAddressesInitializer @Inject constructor( private val dynamicAddressesRepository: DynamicAddressesRepository, private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, private val getDerivedXpubUseCase: GetDerivedXpubUseCase, + private val userWalletsListRepository: UserWalletsListRepository, ) { suspend fun getXpubs(userWalletId: UserWalletId, networks: Set): Map { if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap() + /* + * Dynamic addresses rely on the server-side wallet accounts list, which is populated only for + * multi-currency wallets. Single-currency wallets (Note, s2c, etc.) never populate it, so + * DynamicAddressesRepository.getStatus() — backed by WalletAccountsFetcher.get() — would never + * emit and firstOrNull() below would suspend forever, hanging the whole balance fetch and leaving + * the currency stuck in Loading. Skip such wallets entirely. ([REDACTED_TASK_KEY]) + */ + val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId) + if (userWallet == null || !userWallet.isMultiCurrency) return emptyMap() + val result = mutableMapOf() for (network in networks) { if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue From 438f502a4c857cd22910b9b375f29ea6ffed6b21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 17:26:08 +0200 Subject: [PATCH 13/16] Updated on 2026-08-14 --- .../provider/ProviderTypeFilterPicker.kt | 50 +++++++++---------- .../converters/SwapProviderStateBuilder.kt | 4 ++ .../tangem/feature/swap/model/SwapModel.kt | 3 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 8 +++ .../SwapProviderStateBuilderTest.kt | 44 +++++++++++++++- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt index af6a9062f1..b41decbdab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt @@ -1,7 +1,7 @@ package com.tangem.core.ui.components.provider import androidx.compose.runtime.Composable -import androidx.compose.runtime.key +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tangem.core.ui.R import com.tangem.domain.express.models.ProviderFilterType @@ -20,31 +20,31 @@ fun ProviderTypeFilterPicker( onFilterSelect: (ProviderFilterType) -> Unit, modifier: Modifier = Modifier, ) { - val segments = availableFilters.map { filter -> - TangemSegmentUM( - id = filter.name, - title = when (filter) { - ProviderFilterType.ALL -> resourceReference(R.string.common_all) - ProviderFilterType.CEX -> TextReference.Str("CEX") - ProviderFilterType.DEX -> TextReference.Str("DEX") - }, - ) - }.toImmutableList() - val selectedSegment = segments.firstOrNull { it.id == selectedFilter.name } - TangemThemeRedesign { - // key() forces recomposition when selectedFilter changes to re-seed initialSelectedItem, - // because TangemSegmentedPicker owns its selection state internally via remember. - key(selectedFilter) { - TangemSegmentedPicker( - items = segments, - initialSelectedItem = selectedSegment, - isFixed = true, - modifier = modifier, - onClick = { segment -> - val filterType = availableFilters.firstOrNull { it.name == segment.id } - if (filterType != null) onFilterSelect(filterType) + val segments = remember(availableFilters) { + availableFilters.map { filter -> + TangemSegmentUM( + id = filter.name, + title = when (filter) { + ProviderFilterType.ALL -> resourceReference(R.string.common_all) + ProviderFilterType.CEX -> TextReference.Str("CEX") + ProviderFilterType.DEX -> TextReference.Str("DEX") }, ) - } + }.toImmutableList() + } + val selectedSegment = remember(segments, selectedFilter) { + segments.firstOrNull { it.id == selectedFilter.name } + } + TangemThemeRedesign { + TangemSegmentedPicker( + items = segments, + initialSelectedItem = selectedSegment, + isFixed = true, + modifier = modifier, + onClick = { segment -> + val filterType = availableFilters.firstOrNull { it.name == segment.id } + if (filterType != null) onFilterSelect(filterType) + }, + ) } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt index eccf09da43..5c84a44a72 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt @@ -74,6 +74,8 @@ internal object SwapProviderStateBuilder { permissionState: PermissionDataState, pricesLowerBest: Map, selectionType: ProviderState.SelectionType, + isBestRate: Boolean = false, + isNeedBestRateBadge: Boolean = false, needApplyFCARestrictions: Boolean, onProviderClick: (String) -> Unit, ): ProviderState.Content { @@ -83,6 +85,8 @@ internal object SwapProviderStateBuilder { provider = provider, needApplyFCARestrictions = needApplyFCARestrictions, permissionState = permissionState, + isBestRate = isBestRate, + isNeedBestRateBadge = isNeedBestRateBadge, ), selectionType = selectionType, percentLowerThenBest = pricesLowerBest[provider.providerId] diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index e5cf890a31..975da73307 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1698,12 +1698,15 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ProviderClicked()) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(providerId, states) + val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, selectedProviderId = providerId, pricesLowerBest = pricesLowerBest, providersStates = dataState.lastLoadedSwapStates, needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + bestRatedProviderId = bestRatedProviderId, + isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, onProviderSelect = { providerId -> diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7991737bec..c2cce01561 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1068,6 +1068,8 @@ internal class StateBuilder( pricesLowerBest: Map, providersStates: Map, needApplyFCARestrictions: Boolean, + bestRatedProviderId: String, + isNeedBestRateBadge: Boolean, onDismiss: () -> Unit, ): SwapStateHolder { val availableProvidersStates = providersStates.entries @@ -1076,6 +1078,8 @@ internal class StateBuilder( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, + bestRatedProviderId = bestRatedProviderId, + isNeedBestRateBadge = isNeedBestRateBadge, ) } .sortedWith(ProviderPercentDiffComparator) @@ -1180,6 +1184,8 @@ internal class StateBuilder( pricesLowerBest: Map, onProviderSelect: (String) -> Unit, needApplyFCARestrictions: Boolean, + bestRatedProviderId: String, + isNeedBestRateBadge: Boolean, ): ProviderState? { val provider = this.key return when (val state = this.value) { @@ -1192,6 +1198,8 @@ internal class StateBuilder( pricesLowerBest = pricesLowerBest, selectionType = ProviderState.SelectionType.SELECT, needApplyFCARestrictions = needApplyFCARestrictions, + isBestRate = bestRatedProviderId == provider.providerId && !state.priceImpact.shouldShowWarning(), + isNeedBestRateBadge = isNeedBestRateBadge, onProviderClick = onProviderSelect, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/converters/SwapProviderStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/converters/SwapProviderStateBuilderTest.kt index 6b21165ffe..1e93944a6c 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/converters/SwapProviderStateBuilderTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/converters/SwapProviderStateBuilderTest.kt @@ -220,7 +220,7 @@ internal class SwapProviderStateBuilderTest { } @Test - fun `GIVEN best rate badge inputs WHEN buildContentSelectable THEN BestTrade badge is never set`() { + fun `GIVEN best rate AND no FCA AND no permission WHEN buildContentSelectable THEN BestTrade badge`() { val provider = provider(id = "any", isRecommended = false) val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100")) @@ -231,6 +231,48 @@ internal class SwapProviderStateBuilderTest { pricesLowerBest = emptyMap(), selectionType = ProviderState.SelectionType.SELECT, needApplyFCARestrictions = false, + isBestRate = true, + isNeedBestRateBadge = true, + onProviderClick = onProviderClick, + ) + + assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.BestTrade) + } + + @Test + fun `GIVEN isNeedBestRateBadge false WHEN buildContentSelectable THEN no BestTrade badge`() { + val provider = provider(id = "any", isRecommended = false) + val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100")) + + val result = SwapProviderStateBuilder.buildContentSelectable( + provider = provider, + toTokenInfo = info, + permissionState = PermissionDataState.Empty, + pricesLowerBest = emptyMap(), + selectionType = ProviderState.SelectionType.SELECT, + needApplyFCARestrictions = false, + isBestRate = true, + isNeedBestRateBadge = false, + onProviderClick = onProviderClick, + ) + + assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty) + } + + @Test + fun `GIVEN isBestRate false AND badge enabled WHEN buildContentSelectable THEN no BestTrade badge`() { + val provider = provider(id = "any", isRecommended = false) + val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100")) + + val result = SwapProviderStateBuilder.buildContentSelectable( + provider = provider, + toTokenInfo = info, + permissionState = PermissionDataState.Empty, + pricesLowerBest = emptyMap(), + selectionType = ProviderState.SelectionType.SELECT, + needApplyFCARestrictions = false, + isBestRate = false, + isNeedBestRateBadge = true, onProviderClick = onProviderClick, ) From 0fb88d97c96cfee11b6b60b50d25611ed1d3198b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 21:57:56 +0500 Subject: [PATCH 14/16] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 4 + .../swap/domain/fee/CexSwapFeeCalculator.kt | 67 ++++++++++------- .../SwapInteractorImplLoadSwapFeeTest.kt | 73 ++++++++++++------- .../domain/fee/CexSwapFeeCalculatorTest.kt | 16 ++-- .../tangem/feature/swap/model/SwapModel.kt | 4 +- 6 files changed, 103 insertions(+), 62 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 7f71623656..01cd972d61 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -129,5 +129,6 @@ interface SwapInteractor { amount: SwapAmount, swapData: SwapDataModel?, selectedFeeToken: CryptoCurrencyStatus?, + isGasless: Boolean, ): Either } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f902272fa7..93eaea8fda 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -966,6 +966,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, swapData: SwapDataModel?, selectedFeeToken: CryptoCurrencyStatus?, + isGasless: Boolean, ): Either = either { if (amount.value.signum() == 0) { raise(GetFeeError.UnknownError) @@ -982,6 +983,7 @@ internal class SwapInteractorImpl @Inject constructor( fromStatus = fromStatus, amount = amount, selectedFeeToken = selectedFeeToken, + isGasless = isGasless, ) } } @@ -1031,12 +1033,14 @@ internal class SwapInteractorImpl @Inject constructor( fromStatus: SwapCurrencyStatus, amount: SwapAmount, selectedFeeToken: CryptoCurrencyStatus?, + isGasless: Boolean, ): Either { return cexSwapFeeCalculator.calculate( userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = amount.value, selectedFeeToken = selectedFeeToken, + isGasless = isGasless, ).fold( ifLeft = { it.left() }, ifRight = { cexFeeResult -> diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculator.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculator.kt index a352fd73a7..3bf712d490 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculator.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculator.kt @@ -45,40 +45,51 @@ class CexSwapFeeCalculator( fromSwapCurrencyStatus: SwapCurrencyStatus, amount: BigDecimal, selectedFeeToken: CryptoCurrencyStatus?, + isGasless: Boolean, ): Either = either { if (amount.signum() == 0) { raise(GetFeeError.UnknownError) } - val transactionFeeResult: TransactionFeeResult = when { - selectedFeeToken == null -> { - // Gasless path — overload 1 in SwapInteractorImpl. No gas-limit bump. - val feeExtended = estimateFeeForGaslessTxUseCase( - amount = amount, - userWallet = userWallet, - sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status, - ).bind() - TransactionFeeResult.LoadedExtended(feeExtended) - } - selectedFeeToken.currency is CryptoCurrency.Token -> { - // Explicit gasless-token path — overload 1 in SwapInteractorImpl. No gas-limit bump. - val feeExtended = estimateFeeForTokenUseCase( - userWallet = userWallet, - feeTokenCurrencyStatus = selectedFeeToken, - sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status, - amount = amount, - ).bind() - TransactionFeeResult.LoadedExtended(feeExtended) - } - else -> { - // Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump. - val fee = estimateFeeUseCase( - amount = amount, - userWallet = userWallet, - cryptoCurrencyStatus = fromSwapCurrencyStatus.status, - ).bind() - TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee)) + val transactionFeeResult: TransactionFeeResult = if (isGasless) { + when { + selectedFeeToken == null -> { + // Gasless path — overload 1 in SwapInteractorImpl. No gas-limit bump. + val feeExtended = estimateFeeForGaslessTxUseCase( + amount = amount, + userWallet = userWallet, + sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status, + ).bind() + TransactionFeeResult.LoadedExtended(feeExtended) + } + selectedFeeToken.currency is CryptoCurrency.Token -> { + // Explicit gasless-token path — overload 1 in SwapInteractorImpl. No gas-limit bump. + val feeExtended = estimateFeeForTokenUseCase( + userWallet = userWallet, + feeTokenCurrencyStatus = selectedFeeToken, + sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status, + amount = amount, + ).bind() + TransactionFeeResult.LoadedExtended(feeExtended) + } + else -> { + // Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump. + val fee = estimateFeeUseCase( + amount = amount, + userWallet = userWallet, + cryptoCurrencyStatus = fromSwapCurrencyStatus.status, + ).bind() + TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee)) + } } + } else { + // Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump. + val fee = estimateFeeUseCase( + amount = amount, + userWallet = userWallet, + cryptoCurrencyStatus = fromSwapCurrencyStatus.status, + ).bind() + TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee)) } CexFeeResult(transactionFee = transactionFeeResult) diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadSwapFeeTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadSwapFeeTest.kt index cf265e96d2..e705f7c63e 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadSwapFeeTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadSwapFeeTest.kt @@ -95,6 +95,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = swapData, selectedFeeToken = null, + isGasless = false, ) assertThat(result.isRight()).isTrue() @@ -138,6 +139,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 9), swapData = swapData, selectedFeeToken = null, + isGasless = false, ) assertThat(result.isRight()).isTrue() @@ -173,6 +175,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = swapData, selectedFeeToken = null, + isGasless = false, ) assertThat(result.isRight()).isTrue() @@ -193,6 +196,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, selectedFeeToken = null, + isGasless = false, ) assertThat(result.isLeft()).isTrue() @@ -214,7 +218,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, selectedFeeToken = null, - ) + isGasless = false, + + ) assertThat(result.isLeft()).isTrue() result.onLeft { error -> @@ -240,8 +246,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = swapData, - selectedFeeToken = null, - ) + selectedFeeToken = null, isGasless = false, + + ) assertThat(result.isLeft()).isTrue() result.onLeft { error -> @@ -265,7 +272,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() ) } coEvery { - cexSwapFeeCalculator.calculate(any(), any(), any(), any()) + cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } returns CexFeeResult( transactionFee = TransactionFeeResult.LoadedExtended(extendedFee), ).right() @@ -277,6 +284,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, selectedFeeToken = null, + isGasless = true, ) assertThat(result.isRight()).isTrue() @@ -291,7 +299,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() fromSwapCurrencyStatus = fromStatus, amount = BigDecimal.ONE, selectedFeeToken = null, - ) + isGasless = true, + + ) } } @@ -307,7 +317,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() val toStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) val extendedFee = mockk(relaxed = true) coEvery { - cexSwapFeeCalculator.calculate(any(), any(), any(), any()) + cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } returns CexFeeResult( transactionFee = TransactionFeeResult.LoadedExtended(extendedFee), ).right() @@ -318,8 +328,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, - selectedFeeToken = null, - ) + selectedFeeToken = null, isGasless = true, + + ) assertThat(result.isRight()).isTrue() result.onRight { swapFee -> @@ -337,7 +348,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() } val extendedFee = mockk(relaxed = true) coEvery { - cexSwapFeeCalculator.calculate(any(), any(), any(), any()) + cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } returns CexFeeResult( transactionFee = TransactionFeeResult.LoadedExtended(extendedFee), ).right() @@ -348,8 +359,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, - selectedFeeToken = explicitTokenStatus, - ) + selectedFeeToken = explicitTokenStatus, isGasless = true, + + ) assertThat(result.isRight()).isTrue() result.onRight { swapFee -> @@ -360,8 +372,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal.ONE, - selectedFeeToken = explicitTokenStatus, - ) + selectedFeeToken = explicitTokenStatus, isGasless = true, + + ) } } @@ -374,7 +387,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() } val rawFee = TransactionFee.Single(normal = mockk(relaxed = true)) coEvery { - cexSwapFeeCalculator.calculate(any(), any(), any(), any()) + cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } returns CexFeeResult( transactionFee = TransactionFeeResult.Loaded(rawFee), ).right() @@ -385,8 +398,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, - selectedFeeToken = explicitNativeStatus, - ) + selectedFeeToken = explicitNativeStatus, isGasless = true, + + ) assertThat(result.isRight()).isTrue() result.onRight { swapFee -> @@ -400,7 +414,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() val fromStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) val toStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) coEvery { - cexSwapFeeCalculator.calculate(any(), any(), any(), any()) + cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } returns GetFeeError.UnknownError.left() val result = sut.loadSwapFee( @@ -409,8 +423,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = null, - selectedFeeToken = null, - ) + selectedFeeToken = null, isGasless = true, + + ) assertThat(result.isLeft()).isTrue() result.onLeft { error -> @@ -433,14 +448,15 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ZERO, 18), swapData = null, - selectedFeeToken = null, - ) + selectedFeeToken = null, isGasless = true, + + ) assertThat(result.isLeft()).isTrue() result.onLeft { error -> assertThat(error).isInstanceOf(GetFeeError.UnknownError::class.java) } - coVerify(exactly = 0) { cexSwapFeeCalculator.calculate(any(), any(), any(), any()) } + coVerify(exactly = 0) { cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) } } @Test @@ -459,7 +475,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ZERO, 18), swapData = swapData, selectedFeeToken = null, - ) + isGasless = false, + + ) assertThat(result.isLeft()).isTrue() result.onLeft { error -> @@ -501,7 +519,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() amount = SwapAmount(BigDecimal.ONE, 18), swapData = swapData, selectedFeeToken = explicitTokenStatus, - ) + isGasless = false, + + ) assertThat(result.isRight()).isTrue() result.onRight { swapFee -> @@ -571,8 +591,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase() toStatus = toStatus, amount = SwapAmount(BigDecimal.ONE, 18), swapData = swapData, - selectedFeeToken = null, - ) + selectedFeeToken = null, isGasless = false, + + ) // When resolveNativeFeeTokenStatus returns null → Left(UnknownError) assertThat(result.isLeft()).isTrue() diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculatorTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculatorTest.kt index 6d4e06580a..846dc194ee 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculatorTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/CexSwapFeeCalculatorTest.kt @@ -69,6 +69,7 @@ internal class CexSwapFeeCalculatorTest { fromSwapCurrencyStatus = fromStatus, amount = BigDecimal.ZERO, selectedFeeToken = null, + isGasless = true, ) assertThat(result.isLeft()).isTrue() @@ -97,7 +98,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.5"), - selectedFeeToken = null, + selectedFeeToken = null, isGasless = true, ) assertThat(result.isRight()).isTrue() @@ -130,7 +131,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.0"), - selectedFeeToken = null, + selectedFeeToken = null, isGasless = true, ) assertThat(result.isLeft()).isTrue() @@ -160,7 +161,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("2.0"), - selectedFeeToken = tokenStatus, + selectedFeeToken = tokenStatus, isGasless = true, ) assertThat(result.isRight()).isTrue() @@ -207,7 +208,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("3.0"), - selectedFeeToken = coinStatus, + selectedFeeToken = coinStatus, isGasless = true, ) assertThat(result.isRight()).isTrue() @@ -251,7 +252,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.0"), - selectedFeeToken = coinStatus, + selectedFeeToken = coinStatus, isGasless = true, ) result.onRight { cexResult -> @@ -276,7 +277,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.0"), - selectedFeeToken = coinStatus, + selectedFeeToken = coinStatus, isGasless = true, ) assertThat(result.isLeft()).isTrue() @@ -321,7 +322,7 @@ internal class CexSwapFeeCalculatorTest { userWallet = fromStatus.userWallet, fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.0"), - selectedFeeToken = coinStatus, + selectedFeeToken = coinStatus, isGasless = true, ) result.onRight { cexResult -> @@ -355,6 +356,7 @@ internal class CexSwapFeeCalculatorTest { fromSwapCurrencyStatus = fromStatus, amount = BigDecimal("1.0"), selectedFeeToken = null, + isGasless = true, ) coVerify(exactly = 1) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 975da73307..bbd9023571 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -2176,7 +2176,8 @@ internal class SwapModel @Inject constructor( toStatus = toSwapCurrencyStatus, amount = swapAmount, swapData = swapDataForCall, - selectedFeeToken = dataState.feePaidCryptoCurrency, + selectedFeeToken = null, + isGasless = false, ).map { swapFee -> when (val res = swapFee.transactionFeeResult) { is TransactionFeeResult.LoadedExtended -> res.fee.transactionFee @@ -2230,6 +2231,7 @@ internal class SwapModel @Inject constructor( amount = swapAmount, swapData = swapDataForCall, selectedFeeToken = selectedToken, + isGasless = true, ).map { swapFee -> // The fee selector block consumes TransactionFeeExtended; build one when // `transactionFeeResult` is LoadedExtended, else wrap the native fee in a From ec818bfd0aa7aac3ec37805417c18e28ad44aae4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 20:59:34 +0400 Subject: [PATCH 15/16] Updated on 2026-08-14 --- .../com/tangem/feature/swap/analytics/SwapEvents.kt | 13 +++++++++++++ .../java/com/tangem/feature/swap/model/SwapModel.kt | 1 + 2 files changed, 14 insertions(+) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index c17789bc7a..ceac6df113 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -15,6 +15,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.core.analytics.models.getReferralParams import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.PredefinedPercentAmount import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.FeeBucket @@ -291,4 +292,16 @@ sealed class SwapEvents( "Provider" to provider.name, ), ) + + class FastAmountInput(percent: PredefinedPercentAmount) : SwapEvents( + event = "Fast amount input", + params = mapOf("Percentage" to percent.toAnalyticsValue()), + ) +} + +private fun PredefinedPercentAmount.toAnalyticsValue(): String = when (this) { + PredefinedPercentAmount.PERCENT_25 -> "25" + PredefinedPercentAmount.PERCENT_50 -> "50" + PredefinedPercentAmount.PERCENT_75 -> "75" + PredefinedPercentAmount.MAX -> "Max" } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index bbd9023571..9772880a1c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1522,6 +1522,7 @@ internal class SwapModel @Inject constructor( } private fun onPredefinedPercentSelected(percent: PredefinedPercentAmount) { + analyticsEventHandler.send(SwapEvents.FastAmountInput(percent)) if (percent == PredefinedPercentAmount.MAX) { onMaxAmountClicked() return From 38dcec3f81a4589f2b6ccdc20a86e88df8990c1a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 17:00:00 +0000 Subject: [PATCH 16/16] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0ab9e14901..9323268e46 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.39-1533" +tangemBlockchainSdk = "develop-1535" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.39-623" +tangemCardSdk = "develop-620" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^