Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 21:31:05 +04:00
commit 16ed712a09
77 changed files with 1429 additions and 621 deletions

View file

@ -36,6 +36,9 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
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() }
}
@ -55,8 +58,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() }
@ -65,8 +68,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() }

View file

@ -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<ChooseTokenPageObject>(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)

View file

@ -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<GetTokenBottomSheetPageObject>(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)

View file

@ -55,6 +55,11 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
useUnmergedTree = true
}
val addFundsButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_add_funds))
}
val sendButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_send)))

View file

@ -12,7 +12,7 @@ class TangemPayAddFundsSheetPageObject(semanticsProvider: SemanticsNodeInteracti
ComposeScreen<TangemPayAddFundsSheetPageObject>(semanticsProvider = semanticsProvider) {
val swapOption: KNode = child {
hasText(getResourceString(CoreResR.string.common_exchange))
hasText(getResourceString(CoreResR.string.tangempay_topup_swap_title))
useUnmergedTree = true
}

View file

@ -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() }
}

View file

@ -422,17 +422,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()
@ -481,17 +481,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() }
@ -538,17 +539,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() }

View file

@ -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) }
}
}
}
}

View file

@ -4,10 +4,12 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.swipeVertical
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
@ -86,6 +88,15 @@ class MainScreenTest : BaseTestCase() {
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() }
}
}
}

View file

@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
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()

View file

@ -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}")
}
}

View file

@ -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<String?>()
fun handle(params: Map<String?, Any?>) {
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) {

View file

@ -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

View file

@ -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() }
}
}

View file

@ -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<String?, Any?>, 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<DeepLink> {
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<DeepLink> {
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"

View file

@ -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?,
)

View file

@ -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<TangemPayCard>,
) : 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

View file

@ -91,6 +91,7 @@
<string name="add_custom_token_title">Token anlegen</string>
<string name="add_tokens_title">Token verwalten</string>
<string name="addfunds_buy_row_description">Kreditkarte oder Bankkonto</string>
<string name="addfunds_fund_token">Token erhalten</string>
<string name="addfunds_receive_row_description">Teile deine Adresse oder dein QR-Code</string>
<string name="addfunds_swap_row_description">Zwische deinen Portfolios</string>
<string name="addfunds_you_receive_title">Empfangen</string>
@ -662,6 +663,11 @@
<string name="feedback_subject_support_tangem">Feedback zu Tangem</string>
<string name="feedback_subject_tx_failed">Eine Transaktion kann nicht gesendet werden</string>
<string name="feedback_token_description_error">Fehler in der Coinbeschreibung</string>
<string name="force_update_banner_message">Aktualisiere die Anwendung auf die neueste Version, um die ordnungsgemäße Funktionalität zu gewährleisten</string>
<string name="force_update_banner_title">Aktualisierung erforderlich</string>
<string name="force_update_button">Update</string>
<string name="force_update_warning_message">Bitte aktualisiere die Anwendung auf die neueste Version, um eine einwandfreie Funktion zu gewährleisten.</string>
<string name="force_update_warning_title">Aktualisierung erforderlich</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Nicht genügend Mittel</string>
<string name="gasless_transaction_fee">Transaktionsgebühr</string>
<string name="generic_error">Es ist ein Fehler aufgetreten</string>
@ -787,6 +793,8 @@
<string name="koinos_mana_level_description">Das Koinos-Netzwerk benötigt Mana als Netzwerkgebühr. Du hast %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Mana-Level</string>
<string name="main_add_and_manage_tokens">Hinzufügen und Verwalten</string>
<string name="main_add_funds_promo_description">Krypto einzahlen oder mit Karte kaufen, um loszulegen</string>
<string name="main_add_funds_promo_title">Hol dir deine erste Kryptowährung</string>
<string name="main_empty_tokens_list_message">Um mit der Verfolgung deiner Krypto-Assets und -Transaktionen zu beginnen, füge einen Token hinzu</string>
<string name="main_manage_tokens">Token verwalten</string>
<string name="main_qr_scan_hint">QR-Code scannen, um Geld zu senden oder eine Verbindung zu einer App herzustellen</string>
@ -1073,7 +1081,7 @@
<string name="onboarding_create_wallet_options_button_options">Andere Optionen</string>
<string name="onboarding_create_wallet_options_message">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.</string>
<string name="onboarding_create_wallet_options_title">Schlüssel anonym generieren</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:%s</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:\n%s</string>
<string name="onboarding_done_body">Deine Karte oder Ring ist aktiviert und einsatzbereit</string>
<string name="onboarding_done_header">Erfolgreich!</string>
<string name="onboarding_done_wallet">Deine Wallet ist eingerichtet und einsatzbereit!</string>
@ -1210,9 +1218,7 @@
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="provider_name_support">%s Unterstützung</string>
<string name="push_notification_settings_banner_button_grant_permission">Genehmigung erteilen</string>
<string name="push_notification_settings_banner_description">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast.</string>
<string name="push_notification_settings_banner_description_grant_permission">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst nach Ihrer Zustimmung.</string>
<string name="push_notification_settings_banner_title">Benachrichtigungen zulassen</string>
<string name="push_notification_settings_offers_updates_subtitle">Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten.</string>
<string name="push_notification_settings_offers_updates_title">Angebote &amp; Updates</string>
@ -1684,11 +1690,13 @@
<string name="tangem_pay_freeze_card_failed">Karte konnte nicht eingefroren werden. Versuchen Sie es später erneut.</string>
<string name="tangem_pay_freeze_card_freeze">Einfrieren</string>
<string name="tangem_pay_freeze_card_success">Ihre Karte ist eingefroren.</string>
<string name="tangem_pay_freeze_card_unfreeze">Aufheben</string>
<string name="tangem_pay_get_help">Hilfe erhalten</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Grund: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Andere</string>
<string name="tangem_pay_pin_code_title">PIN-Code</string>
<string name="tangem_pay_rooted_device_subtitle">Nicht nutzbar auf gerooteten Geräten</string>
<string name="tangem_pay_status_completed">Abgeschlossen</string>
<string name="tangem_pay_status_declined">Abgelehnt</string>
@ -1697,15 +1705,15 @@
<string name="tangem_pay_terms_fees_limits">Bedingungen, Gebühren &amp; Limits</string>
<string name="tangem_pay_terms_limits">Bedingungen und Einschränkungen</string>
<string name="tangem_pay_transaction_declined_notification_text">Die Bank hat diese Transaktionsanfrage abgelehnt.</string>
<string name="tangem_pay_transaction_fee_notification_text">Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung.</string>
<string name="tangem_pay_transaction_fee_notification_text">Eine Gebühr wird gemäß den Servicetarifen erhoben</string>
<string name="tangem_pay_transaction_reversed_notification_text">Die Transaktion wurde vom Händler teilweise oder vollständig storniert</string>
<string name="tangem_pay_unfreeze_card_alert_body">Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Karte entsperren?</string>
<string name="tangem_pay_unfreeze_card_failed">Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut.</string>
<string name="tangem_pay_unfreeze_card_success">Ihre Karte ist entsperrt.</string>
<string name="tangem_pay_withdrawal">Abhebung</string>
<string name="tangempay_account_deactivated_message_subtitle">Dies wurde aufgrund regulatorischer Anforderungen durchgeführt. Auszahlungen sind jedoch weiterhin verfügbar.</string>
<string name="tangempay_account_deactivated_message_title">Ihre Karte wurde deaktiviert</string>
<string name="tangempay_account_deactivated_message_subtitle">Bei Fragen zu Ihrem Konto, Ihren Daten oder Ihrem Transaktionsverlauf wenden Sie sich bitte an den Support</string>
<string name="tangempay_account_deactivated_message_title">Ihr Konto wurde geschlossen</string>
<string name="tangempay_account_unable_to_use_rooted">Auf gerooteten Geräten nicht nutzbar.</string>
<string name="tangempay_available_balance">Verfügbares Guthaben</string>
<string name="tangempay_cancel_kyc">KYC vom Hauptbildschirm ausblenden</string>
@ -1739,7 +1747,6 @@
<string name="tangempay_card_details_open_wallet_title">Karte zu Google Pay hinzufügen</string>
<string name="tangempay_card_details_open_wallet_title_apple">Karte zu Apple Pay hinzufügen</string>
<string name="tangempay_card_details_pin_code">Pin Code</string>
<string name="tangempay_card_details_receive_description">Teile Deine Adresse mit oder zeig den QR-Code.</string>
<string name="tangempay_card_details_receive_error_description">Es wurden technische Probleme festgestellt. Bitte versuche es später erneut oder kontaktiere den Support.</string>
<string name="tangempay_card_details_receive_error_title">Empfangen ist jetzt nicht verfügbar</string>
<string name="tangempay_card_details_reissue_card">Karte neu ausstellen</string>
@ -1748,7 +1755,6 @@
<string name="tangempay_card_details_rename_card_placeholder">Kartenname</string>
<string name="tangempay_card_details_reveal_text">Aufdecken</string>
<string name="tangempay_card_details_show_details">Details anzeigen</string>
<string name="tangempay_card_details_swap_description">Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte.</string>
<string name="tangempay_card_details_title">Kartendetails</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Bitte versuche es später noch einmal.</string>
<string name="tangempay_card_details_unfreeze_card">Karte entsperren</string>
@ -1766,6 +1772,7 @@
<string name="tangempay_card_page_daily_limit_change">Ändern</string>
<string name="tangempay_card_page_daily_limit_current_limit">Aktuelles Limit</string>
<string name="tangempay_card_page_daily_limit_error_description">Ihr Tageslimit konnte nicht geladen werden. Bitte versuchen Sie es erneut.</string>
<string name="tangempay_card_page_daily_limit_error_subtitle">Neu laden und es erneut versuchen</string>
<string name="tangempay_card_page_daily_limit_error_title">Tageslimit nicht verfügbar</string>
<string name="tangempay_card_page_daily_limit_success_description">Sie können es jederzeit wieder ändern</string>
<string name="tangempay_card_page_daily_limit_success_title">Tageslimit ist festgelegt</string>
@ -1827,7 +1834,7 @@
<string name="tangempay_onboarding_security_title">Unerreichte Privatsphäre</string>
<string name="tangempay_onboarding_setup_wallet_description">Verknüpfen Sie eine Zahlungskarte</string>
<string name="tangempay_onboarding_setup_wallet_title">Wir richten eine Wallet ein.</string>
<string name="tangempay_onboarding_title">Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten</string>
<string name="tangempay_onboarding_title">Holen Sie sich Ihre Tangem Pay Karte</string>
<string name="tangempay_pay_support">Bezahlen mit</string>
<string name="tangempay_payment_account">Zahlungskonto</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay sitzung abgelaufen</string>
@ -1851,7 +1858,6 @@
<string name="tangempay_sync_needed">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
<string name="tangempay_sync_needed_body">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
<string name="tangempay_sync_needed_button">Zugang wiederherstellen</string>
<string name="tangempay_sync_needed_restore_access">Zugang wiederherstellen</string>
<string name="tangempay_sync_needed_title">Tangem Pay sitzung abgelaufen</string>
<string name="tangempay_tangem_visa_card">Nutzen Sie USDC für alltägliche Zahlungen</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht erreichbar.</string>
@ -1861,7 +1867,6 @@
<string name="tangempay_topup_swap_body">Tauschen Sie beliebige Assets in USDC Polygon um</string>
<string name="tangempay_topup_swap_title">Aus Ihrer Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC im Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen</string>
<string name="tangempay_withdrawal_note_description">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</string>
<string name="tangempay_withdrawal_note_title">Bitte beachten Sie</string>
<string name="tangempay_your_pin_code">Ihr PIN-Code</string>

View file

@ -370,6 +370,7 @@
<string name="common_select_action">Seleccione una acción</string>
<string name="common_sell">Vender</string>
<string name="common_send">Enviar</string>
<string name="common_send_colon">Enviar:</string>
<string name="common_send_tx_error">Error al enviar la transacción</string>
<string name="common_server_unavailable">El servidor no está disponible, por favor inténtelo de nuevo más tarde</string>
<string name="common_share">Compartir</string>
@ -1609,15 +1610,15 @@
<string name="tangem_pay_terms_fees_limits">Términos, tarifas y límites</string>
<string name="tangem_pay_terms_limits">Términos y límites</string>
<string name="tangem_pay_transaction_declined_notification_text">El banco rechazó esta solicitud de transacción.</string>
<string name="tangem_pay_transaction_fee_notification_text">Esta tarifa cubre el costo de procesar tu transferencia.</string>
<string name="tangem_pay_transaction_fee_notification_text">Se cobra una comisión de acuerdo con las tarifas de servicio</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transacción fue revertida parcial o totalmente por el comerciante</string>
<string name="tangem_pay_unfreeze_card_alert_body">Sigue usando tu dinero. Puedes congelarlo en cualquier momento.</string>
<string name="tangem_pay_unfreeze_card_alert_title">¿Descongelar tu tarjeta?</string>
<string name="tangem_pay_unfreeze_card_failed">No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde.</string>
<string name="tangem_pay_unfreeze_card_success">Tu tarjeta está descongelada.</string>
<string name="tangem_pay_withdrawal">Retirada</string>
<string name="tangempay_account_deactivated_message_subtitle">Esto se hizo debido a requisitos regulatorios. Sin embargo, los retiros siguen estando disponibles.</string>
<string name="tangempay_account_deactivated_message_title">Su tarjeta ha sido desactivada</string>
<string name="tangempay_account_deactivated_message_subtitle">Para consultas sobre su cuenta, datos o historial de transacciones, contacte con el soporte</string>
<string name="tangempay_account_deactivated_message_title">Su cuenta ha sido cerrada</string>
<string name="tangempay_account_unable_to_use_rooted">No se puede usar en un dispositivo rooteado</string>
<string name="tangempay_available_balance">Saldo</string>
<string name="tangempay_cancel_kyc">Ocultar verificación de la pantalla</string>
@ -1650,7 +1651,6 @@
<string name="tangempay_card_details_open_wallet_title">Añadir tarjeta a Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Añade tu tarjeta a Apple Pay</string>
<string name="tangempay_card_details_pin_code">Código PIN</string>
<string name="tangempay_card_details_receive_description">Comparte tu dirección o muestra el código QR</string>
<string name="tangempay_card_details_receive_error_description">Se detectaron problemas técnicos. Inténtelo de nuevo más tarde o póngase en contacto con el servicio de asistencia.</string>
<string name="tangempay_card_details_receive_error_title">Recepción no disponible ahora</string>
<string name="tangempay_card_details_reissue_card">Reemitir tarjeta</string>
@ -1658,7 +1658,6 @@
<string name="tangempay_card_details_rename_card_invalid_title">Caracteres no válidos</string>
<string name="tangempay_card_details_reveal_text">Mostrar</string>
<string name="tangempay_card_details_show_details">Mostrar detalles</string>
<string name="tangempay_card_details_swap_description">Intercambia cualquier activo de tu portafolio por una tarjeta</string>
<string name="tangempay_card_details_title">Detalles de la tarjeta</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Por favor, inténtalo de nuevo más tarde</string>
<string name="tangempay_card_details_unfreeze_card">Descongelar tarjeta</string>
@ -1719,7 +1718,7 @@
<string name="tangempay_onboarding_purchases_title">Paga exactamente lo que ves</string>
<string name="tangempay_onboarding_security_description">Se creará una cuenta de pago separada sin divulgar tus direcciones y activos</string>
<string name="tangempay_onboarding_security_title">Privacidad inigualable</string>
<string name="tangempay_onboarding_title">Obtén tu tarjeta Tangem Pay gratuita en minutos</string>
<string name="tangempay_onboarding_title">Obtén tu tarjeta Tangem Pay en minutos</string>
<string name="tangempay_payment_account">Cuenta de pago</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay sesión expirada</string>
<string name="tangempay_pin_validation_error_message">PIN no válido: evitar secuencias o repeticiones</string>
@ -1741,7 +1740,6 @@
<string name="tangempay_sync_needed">Usa la tarjeta o el anillo para renovar la sesión</string>
<string name="tangempay_sync_needed_body">Usa la tarjeta o el anillo para renovar la sesión</string>
<string name="tangempay_sync_needed_button">Restablecer acceso</string>
<string name="tangempay_sync_needed_restore_access">Restablecer acceso</string>
<string name="tangempay_sync_needed_title">Tangem Pay sesión expirada</string>
<string name="tangempay_tangem_visa_card">Usa USDC para pagos cotidianos</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</string>
@ -1751,7 +1749,6 @@
<string name="tangempay_topup_swap_body">Intercambia cualquier activo por USDC Polygon</string>
<string name="tangempay_topup_swap_title">Desde tu Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC en Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Haga clic en el botón de abajo para restaurar el acceso</string>
<string name="tangempay_withdrawal_note_description">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</string>
<string name="tangempay_withdrawal_note_title">Tenga en cuenta</string>
<string name="tangempay_your_pin_code">Tu código PIN</string>

View file

@ -80,6 +80,7 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Ajouter des jetons</string>
<string name="action_buttons_you_want_to_receive">Sélectionnez le jeton que vous souhaitez recevoir</string>
<string name="action_buttons_you_want_to_swap">Sélectionnez le jeton que vous souhaitez échanger</string>
<string name="add_and_manage_sheet_manage_title">Ajouter des jetons</string>
<string name="add_custom_token_choose_network">Choisissez le réseau</string>
<string name="add_custom_token_title">Ajouter un jeton personnalisé</string>
<string name="add_tokens_title">Gérer les jetons</string>
@ -367,6 +368,7 @@
<string name="common_select_action">Sélectionnez une action</string>
<string name="common_sell">Vendre</string>
<string name="common_send">Envoyer</string>
<string name="common_send_colon">Vous envoyez :</string>
<string name="common_send_tx_error">Échec d\'envoi de la transaction</string>
<string name="common_server_unavailable">Le serveur n\'est pas disponible, veuillez réessayer plus tard</string>
<string name="common_share">Partager</string>
@ -549,6 +551,7 @@
<string name="express_provider">Fournisseur</string>
<string name="express_provider_best_rate">Meilleur taux</string>
<string name="express_provider_fca_warning_list">Liste davertissement de la FCA</string>
<string name="express_provider_for_swap">Prestataire pour l\'échange</string>
<string name="express_provider_great_rate">Meilleur choix</string>
<string name="express_provider_in_fca_warning_list">Fournisseur figurant sur la liste d\'avertissement de la FCA</string>
<string name="express_provider_max_amount">Disponible jusqu\'à %s</string>
@ -711,6 +714,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Limite de Mana</string>
<string name="koinos_mana_level_description">Le réseau Koinos nécessite du Mana pour les frais de réseau. Vous avez %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Quantité de Mana</string>
<string name="main_add_and_manage_tokens">Ajouter &amp; gérer</string>
<string name="main_empty_tokens_list_message">Pour commencer à suivre vos actifs et transactions crypto, ajoutez des jetons</string>
<string name="main_manage_tokens">Gérer les jetons</string>
<string name="main_scan_card_warning_view_subtitle">Pour accéder à tous les réseaux, vous devez scanner la carte</string>
@ -1047,16 +1051,29 @@
<string name="onramp_error_transaction_already_processed">Cette transaction a déjà été traitée. Aucune autre action n\'est requise.</string>
<string name="onramp_fetching_best_rates">Recherche des meilleurs tarifs...</string>
<string name="onramp_instant_status">Instantané</string>
<string name="onramp_kyc_verification_bullet_free">La vérification est gratuite et prend généralement entre 1 et 2 minutes</string>
<string name="onramp_kyc_verification_bullet_privacy">Tangem n\'a pas accès à vos données personnelles, vous les partagez directement au prestataire agréé</string>
<string name="onramp_kyc_verification_bullet_unlocks">La vérification vous donne un accès complet aux futures transactions avec ce prestataire</string>
<string name="onramp_kyc_verification_choose_another">Sélectionner une autre méthode</string>
<string name="onramp_kyc_verification_subtitle">Conformément aux exigences réglementaires locales, %@ exige une vérification d\'identité.</string>
<string name="onramp_kyc_verification_title">Vérification d\'identité requise par le prestataire de paiement</string>
<string name="onramp_kyc_verification_verify_button">Passer la vérification</string>
<string name="onramp_kyc_verification_whats_important">Ce qui est important</string>
<string name="onramp_legal">En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur</string>
<string name="onramp_legal_text">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
<string name="onramp_max_amount_restriction">Le montant de l\'achat ne doit pas dépasser %s</string>
<string name="onramp_min_amount_restriction">Le montant à acheter doit être au moins %s</string>
<string name="onramp_native_payment_cumulative_limit" formatted="false">Si le montant cumulé des transactions dépasse %1s, une vérification d\'identité via %2s pourrait être requise</string>
<string name="onramp_native_payment_cumulative_limit_equivalent" formatted="false">Si le montant cumulé des transactions dépasse l\'équivalent de %1s, une vérification d\'identité via %2s pourrait être requise</string>
<string name="onramp_native_payment_legal_notice" formatted="false">En appuyant sur Acheter, vous acceptez %1s %2s et %3s.</string>
<string name="onramp_no_available_providers">Aucun fournisseur disponible pour cette devise</string>
<string name="onramp_offer_type_fastet">Le plus rapide</string>
<string name="onramp_pay_with">Payer avec</string>
<string name="onramp_payment_method_subtitle">Mode de paiement</string>
<string name="onramp_provider_max_amount">Disponible jusqu\'à %s</string>
<string name="onramp_provider_min_amount">Disponible à partir de %s</string>
<string name="onramp_provider_requirements_body">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</string>
<string name="onramp_provider_requirements_title">Exigences du prestataire</string>
<plurals name="onramp_providers_count">
<item quantity="one">%d fournisseur</item>
<item quantity="other">%d fournisseurs</item>
@ -1456,6 +1473,7 @@
<string name="sui_not_enough_coin_for_fee_description">Une transaction entrante d\'au moins de %1$s est requise pour continuer</string>
<string name="sui_not_enough_coin_for_fee_title">Fonds insuffisants</string>
<string name="swap_approve_description">En approuvant, vous autorisez le contrat intelligent à utiliser vos jetons dans de futures transactions.</string>
<string name="swap_detailed_mode">Mode détaillé</string>
<string name="swap_fixed_rate">Taux fixe</string>
<string name="swap_give_permission_fee_footer">Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange.</string>
<string name="swap_in_progress">Échange en cours</string>
@ -1463,6 +1481,7 @@
<string name="swap_promo_title">Nouveau fournisseur d\'échange disponible !</string>
<string name="swap_search_tooltip_description">Recherchez nimporte quel token, même sil ne figure pas encore dans votre liste.</string>
<string name="swap_search_tooltip_title">Utilisez la recherche pour trouver ce dont vous avez besoin.</string>
<string name="swap_simple_mode">Mode simplifié</string>
<string name="swap_story_fifth_subtitle">Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes</string>
<string name="swap_story_fifth_title">Assistance 24 heures sur 24</string>
<string name="swap_story_first_subtitle">Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement</string>
@ -1534,15 +1553,15 @@
<string name="tangem_pay_terms_fees_limits">Conditions, frais et limites</string>
<string name="tangem_pay_terms_limits">Conditions et limites</string>
<string name="tangem_pay_transaction_declined_notification_text">La banque a rejeté cette demande de transaction.</string>
<string name="tangem_pay_transaction_fee_notification_text">Ces frais couvrent le coût du traitement de votre virement.</string>
<string name="tangem_pay_transaction_fee_notification_text">Des frais sont prélevés conformément aux tarifs de service</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transaction a été partiellement ou totalement annulée par le commerçant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Continuez à utiliser votre argent. Vous pouvez le geler à tout moment.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Dégeler votre carte ?</string>
<string name="tangem_pay_unfreeze_card_failed">Échec du dégel de la carte. Réessayez plus tard.</string>
<string name="tangem_pay_unfreeze_card_success">Votre carte est dégelée.</string>
<string name="tangem_pay_withdrawal">Retrait</string>
<string name="tangempay_account_deactivated_message_subtitle">Cela a été fait conformément aux exigences réglementaires. Toutefois, les retraits restent disponibles.</string>
<string name="tangempay_account_deactivated_message_title">Votre carte a été désactivée</string>
<string name="tangempay_account_deactivated_message_subtitle">Pour toute question concernant votre compte, vos données ou votre historique de transactions, veuillez contacter le support</string>
<string name="tangempay_account_deactivated_message_title">Votre compte a été fermé</string>
<string name="tangempay_account_unable_to_use_rooted">Impossible à utiliser sur un appareil rooté</string>
<string name="tangempay_available_balance">Solde</string>
<string name="tangempay_cancel_kyc">Masquer la vérification de l\'écran</string>
@ -1574,7 +1593,6 @@
<string name="tangempay_card_details_open_wallet_title">Ajouter une carte à Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Ajouter la carte à Apple Pay</string>
<string name="tangempay_card_details_pin_code">code PIN</string>
<string name="tangempay_card_details_receive_description">Partagez votre adresse ou montrez le QR code</string>
<string name="tangempay_card_details_receive_error_description">Problèmes techniques détectés. Veuillez réessayer plus tard ou contacter le service d\'assistance.</string>
<string name="tangempay_card_details_receive_error_title">Réception indisponible pour le moment</string>
<string name="tangempay_card_details_reissue_card">Réémettre la carte</string>
@ -1582,7 +1600,6 @@
<string name="tangempay_card_details_rename_card_invalid_title">Caractères non valides</string>
<string name="tangempay_card_details_reveal_text">Révéler</string>
<string name="tangempay_card_details_show_details">Afficher les détails</string>
<string name="tangempay_card_details_swap_description">Échangez n\'importe quel actif de votre portefeuille contre une carte</string>
<string name="tangempay_card_details_title">Détails de la carte</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Veuillez réessayer plus tard</string>
<string name="tangempay_card_details_unfreeze_card">Dégeler la carte</string>
@ -1643,7 +1660,7 @@
<string name="tangempay_onboarding_purchases_title">Payez exactement ce que vous voyez</string>
<string name="tangempay_onboarding_security_description">Un compte de paiement séparé sera créé sans divulguer vos adresses et actifs</string>
<string name="tangempay_onboarding_security_title">Confidentialité inégalée</string>
<string name="tangempay_onboarding_title">Obtenez votre carte Tangem Pay gratuite en quelques minutes</string>
<string name="tangempay_onboarding_title">Obtenez votre carte Tangem Pay en minutes</string>
<string name="tangempay_payment_account">Compte de paiement</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay session expirée</string>
<string name="tangempay_pin_validation_error_message">Code PIN invalide : évitez les séquences ou les répétitions</string>
@ -1665,7 +1682,6 @@
<string name="tangempay_sync_needed">Utilisez carte ou bague pour renouveler la session</string>
<string name="tangempay_sync_needed_body">Utilisez carte ou bague pour renouveler la session</string>
<string name="tangempay_sync_needed_button">Restaurer l\'accès</string>
<string name="tangempay_sync_needed_restore_access">Restaurer l\'accès</string>
<string name="tangempay_sync_needed_title">Tangem Pay session expirée</string>
<string name="tangempay_tangem_visa_card">Utilisez USDC pour les paiements quotidiens</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay est temporairement indisponible</string>
@ -1675,7 +1691,6 @@
<string name="tangempay_topup_swap_body">Échangez n\'importe quel actif contre USDC Polygon</string>
<string name="tangempay_topup_swap_title">Depuis votre Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC sur Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Cliquez sur le bouton ci-dessous pour restaurer l\'accès</string>
<string name="tangempay_withdrawal_note_description">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</string>
<string name="tangempay_withdrawal_note_title">Veuillez noter</string>
<string name="tangempay_your_pin_code">Votre code PIN</string>

View file

@ -94,15 +94,15 @@
<string name="tangem_pay_terms_fees_limits">Termini, commissioni e limiti</string>
<string name="tangem_pay_terms_limits">Termini e limiti</string>
<string name="tangem_pay_transaction_declined_notification_text">La banca ha rifiutato questa richiesta di transazione.</string>
<string name="tangem_pay_transaction_fee_notification_text">Questa commissione copre il costo della gestione del tuo trasferimento.</string>
<string name="tangem_pay_transaction_fee_notification_text">Viene addebitata una commissione in base alle tariffe del servizio</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transazione è stata parzialmente o totalmente stornata dal commerciante</string>
<string name="tangem_pay_unfreeze_card_alert_body">Continua a usare i tuoi soldi. Puoi congelarli in qualsiasi momento.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Sbloccare la tua carta?</string>
<string name="tangem_pay_unfreeze_card_failed">Impossibile sbloccare la carta. Riprova più tardi.</string>
<string name="tangem_pay_unfreeze_card_success">La tua carta è sbloccata.</string>
<string name="tangem_pay_withdrawal">Prelievo</string>
<string name="tangempay_account_deactivated_message_subtitle">Questo è stato fatto a causa dei requisiti normativi. Tuttavia, i prelievi sono ancora disponibili.</string>
<string name="tangempay_account_deactivated_message_title">La tua carta è stata disattivata</string>
<string name="tangempay_account_deactivated_message_subtitle">Per domande su account, dati o cronologia delle transazioni, contatta il supporto</string>
<string name="tangempay_account_deactivated_message_title">Il tuo account è stato chiuso</string>
<string name="tangempay_available_balance">Saldo</string>
<string name="tangempay_cancel_kyc">Nascondi verifica dalla schermata</string>
<string name="tangempay_card_details_add_funds">Aggiungi fondi</string>
@ -132,14 +132,12 @@
<string name="tangempay_card_details_open_wallet_step_5">Tutto pronto! La tua carta è pronta per l\'uso.</string>
<string name="tangempay_card_details_open_wallet_title">Aggiungi carta a Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Aggiungi carta ad Apple Pay</string>
<string name="tangempay_card_details_receive_description">Condividi il tuo indirizzo o mostra il QR code</string>
<string name="tangempay_card_details_receive_error_title">Ricezione non disponibile al momento</string>
<string name="tangempay_card_details_reissue_card">Riemettere la carta</string>
<string name="tangempay_card_details_rename_card_invalid_description">Sono consentite solo lettere e numeri</string>
<string name="tangempay_card_details_rename_card_invalid_title">Caratteri non validi</string>
<string name="tangempay_card_details_reveal_text">Rivela</string>
<string name="tangempay_card_details_show_details">Mostra dettagli</string>
<string name="tangempay_card_details_swap_description">Scambia qualsiasi asset nel tuo portafoglio con una carta</string>
<string name="tangempay_card_details_title">Dettagli carta</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Per favore riprova più tardi</string>
<string name="tangempay_card_details_unfreeze_card">Sblocca carta</string>
@ -194,7 +192,7 @@
<string name="tangempay_onboarding_purchases_title">Paga esattamente quello che vedi</string>
<string name="tangempay_onboarding_security_description">Verrà creato un conto di pagamento separato senza divulgare i tuoi indirizzi e asset</string>
<string name="tangempay_onboarding_security_title">Privacy senza rivali</string>
<string name="tangempay_onboarding_title">Ottieni la tua carta Tangem Pay gratuita in pochi minuti</string>
<string name="tangempay_onboarding_title">Ottieni la tua carta Tangem Pay in pochi minuti</string>
<string name="tangempay_payment_account">Conto di pagamento</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay sessione scaduta</string>
<string name="tangempay_pin_validation_error_message">PIN non valido: evitare sequenze o ripetizioni</string>
@ -223,7 +221,6 @@
<string name="tangempay_topup_swap_body">Converti qualsiasi asset in USDC Polygon</string>
<string name="tangempay_topup_swap_title">Dal tuo Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC sulla Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Fare clic sul pulsante in basso per ripristinare l\'accesso</string>
<string name="tangempay_withdrawal_note_description">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</string>
<string name="tangempay_withdrawal_note_title">Attenzione</string>
<string name="tangempay_your_pin_code">Il tuo codice PIN</string>

View file

@ -393,6 +393,7 @@
<string name="common_sending">送金中</string>
<string name="common_sent">送金済み</string>
<string name="common_server_unavailable">サーバーが利用できません。しばらくしてからもう一度お試しください。</string>
<string name="common_session_expired">セッションの有効期限が切れました</string>
<string name="common_share">共有</string>
<string name="common_share_link">リンクを共有</string>
<string name="common_show_less">詳細を非表示</string>
@ -776,6 +777,8 @@
<string name="koinos_mana_level_description">Koinosネットワークでは、ネットワーク手数料としてManaが必要です。あなたは%1$s / %2$sManaを持っています。</string>
<string name="koinos_mana_level_title">Manaレベル</string>
<string name="main_add_and_manage_tokens">追加・管理</string>
<string name="main_add_funds_promo_description">暗号資産を入金またはカードで購入</string>
<string name="main_add_funds_promo_title">入金して、運用や取引を始めましょう。</string>
<string name="main_empty_tokens_list_message">暗号資産および取引の追跡を開始するには、トークンを追加してください</string>
<string name="main_manage_tokens">トークンの管理</string>
<string name="main_qr_scan_hint">QRコードをスキャンして送金するか、アプリに接続します。</string>
@ -1187,9 +1190,7 @@
<string name="organize_tokens_title">トークンを整理する</string>
<string name="organize_tokens_ungroup">グループ解除</string>
<string name="provider_name_support">%sサポート</string>
<string name="push_notification_settings_banner_button_grant_permission">許可する</string>
<string name="push_notification_settings_banner_description">プッシュ通知は有効になっていますが、端末の設定で通知を許可するまで動作しません。</string>
<string name="push_notification_settings_banner_description_grant_permission">プッシュ通知は有効になっていますが、許可するまで機能しません</string>
<string name="push_notification_settings_banner_description">プッシュ通知は有効ですが、許可するまで動作しません</string>
<string name="push_notification_settings_banner_title">通知を許可する</string>
<string name="push_notification_settings_offers_updates_subtitle">製品ニュース、限定オファー、アクティビティのリマインダー。</string>
<string name="push_notification_settings_offers_updates_title">オファー・最新情報</string>
@ -1628,7 +1629,7 @@
<string name="swapping_rate_experience_title">プロバイダーの利用体験を評価してください</string>
<string name="swapping_rate_feedback_placeholder">フィードバックを入力してください</string>
<string name="swapping_rate_feedback_submit">フィードバックを送信</string>
<string name="swapping_rate_feedback_title">ご利用体験に影響した点は\n何ですか</string>
<string name="swapping_rate_feedback_title">ご利用中に気になった点を\n教えてください</string>
<string name="swapping_swap_action">スワップ</string>
<string name="swapping_swap_action_in_progress">スワップ中…</string>
<string name="swapping_to_account_title">受け取り先</string>
@ -1671,15 +1672,15 @@
<string name="tangem_pay_terms_fees_limits">利用規約・手数料・利用制限</string>
<string name="tangem_pay_terms_limits">利用規約と手数料</string>
<string name="tangem_pay_transaction_declined_notification_text">銀行がこの取引リクエストを拒否しました。</string>
<string name="tangem_pay_transaction_fee_notification_text">この手数料は、送金処理にかかるコストをカバーするためのものです。</string>
<string name="tangem_pay_transaction_fee_notification_text">手数料はサービス料金に基づいて請求されます</string>
<string name="tangem_pay_transaction_reversed_notification_text">この取引は加盟店により一部または全額取り消されました</string>
<string name="tangem_pay_unfreeze_card_alert_body">資金は引き続き使用できます。いつでも一時停止できます。</string>
<string name="tangem_pay_unfreeze_card_alert_title">カードの一時停止を解除しますか?</string>
<string name="tangem_pay_unfreeze_card_failed">カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。</string>
<string name="tangem_pay_unfreeze_card_success">カードの凍結が解除されました</string>
<string name="tangem_pay_withdrawal">出金</string>
<string name="tangempay_account_deactivated_message_subtitle">規制上の要件により無効化されましたが、出金は引き続き可能です。</string>
<string name="tangempay_account_deactivated_message_title">カードが無効化されました</string>
<string name="tangempay_account_deactivated_message_subtitle">アカウント、データ、または取引履歴に関するご質問は、サポートまでご連絡ください</string>
<string name="tangempay_account_deactivated_message_title">あなたのアカウントは閉鎖されました</string>
<string name="tangempay_account_unable_to_use_rooted">Root化された端末では使用できません</string>
<string name="tangempay_available_balance">利用可能残高</string>
<string name="tangempay_cancel_kyc">メイン画面からKYCを非表示にする</string>
@ -1713,7 +1714,6 @@
<string name="tangempay_card_details_open_wallet_title">Google Payにカードを追加する</string>
<string name="tangempay_card_details_open_wallet_title_apple">Apple Payにカードを追加する</string>
<string name="tangempay_card_details_pin_code">PINコード</string>
<string name="tangempay_card_details_receive_description">アドレスを共有するか、QRコードを表示してください。</string>
<string name="tangempay_card_details_receive_error_description">技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。</string>
<string name="tangempay_card_details_receive_error_title">現在、受け取りは利用できません</string>
<string name="tangempay_card_details_reissue_card">カードを交換する</string>
@ -1722,7 +1722,6 @@
<string name="tangempay_card_details_rename_card_placeholder">カード名</string>
<string name="tangempay_card_details_reveal_text">表示</string>
<string name="tangempay_card_details_show_details">詳細を表示</string>
<string name="tangempay_card_details_swap_description">ポートフォリオ内のあらゆる資産をカードと交換</string>
<string name="tangempay_card_details_title">カードの詳細</string>
<string name="tangempay_card_details_unable_to_rename_card_description">しばらくしてからもう一度お試しください</string>
<string name="tangempay_card_details_unfreeze_card">カードの一時停止を解除</string>
@ -1800,7 +1799,7 @@
<string name="tangempay_onboarding_security_title">他に類を見ないプライバシー</string>
<string name="tangempay_onboarding_setup_wallet_description">そして支払いカードを連携します</string>
<string name="tangempay_onboarding_setup_wallet_title">ウォレットを設定します</string>
<string name="tangempay_onboarding_title">無料のTangem Payカードを数分でゲットしましょ</string>
<string name="tangempay_onboarding_title">Tangem Pay カードをすぐに手に入れよ</string>
<string name="tangempay_pay_support">Payサポート</string>
<string name="tangempay_payment_account">支払いアカウント</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay セッションの有効期限が切れました</string>
@ -1824,17 +1823,15 @@
<string name="tangempay_sync_needed">カードまたはリングでセッションを更新してください</string>
<string name="tangempay_sync_needed_body">カードまたはリングでセッションを更新してください</string>
<string name="tangempay_sync_needed_button">セッションを更新</string>
<string name="tangempay_sync_needed_restore_access">セッションを更新</string>
<string name="tangempay_sync_needed_title">Tangem Pay セッションの有効期限が切れました</string>
<string name="tangempay_tangem_visa_card">日常の支払いにUSDCを利用</string>
<string name="tangempay_temporarily_unavailable">Tangem Payは現在一時的に利用できません。</string>
<string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">USDC Polygon をアカウントのアドレスに送信</string>
<string name="tangempay_topup_receive_title">別のウォレットまたは取引所から</string>
<string name="tangempay_topup_swap_body">任意の資産を USDC Polygon にスワップ</string>
<string name="tangempay_topup_swap_title">Tangem ウォレットから</string>
<string name="tangempay_topup_swap_body">ウォレットの暗号資産を使って、決済アカウントにチャージできます</string>
<string name="tangempay_topup_swap_title">Tangemウォレットからスワップ</string>
<string name="tangempay_usdc_on_polygon_network">Polygonネットワーク上のUSDC</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">下のボタンをクリックしてアクセスを復元してください</string>
<string name="tangempay_withdrawal_note_description">返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。</string>
<string name="tangempay_withdrawal_note_title">ご注意ください</string>
<string name="tangempay_your_pin_code">PINコード</string>
@ -2346,6 +2343,10 @@
<string name="yield_apy_boost_banner_title">利息モード限定オファー</string>
<string name="yield_apy_boost_banner_title_apy_multiplied">APY 3倍</string>
<string name="yield_apy_boost_block_activate">APYブーストを有効にする</string>
<string name="yield_apy_boost_promo_activate_bonus">ボーナスを有効にする</string>
<string name="yield_apy_boost_promo_bonus_paid_out_subtitle">詳細は取引履歴をご確認ください</string>
<string name="yield_apy_boost_promo_bonus_paid_out_title">利息モードのボーナスが支払われました</string>
<string name="yield_apy_boost_promo_days_left_to_unlock">ボーナス獲得まであと%1$s日</string>
<string name="yield_apy_boost_promo_eligibility_text">30日間APYブーストの対象です。利用規約が適用されます。詳細はこちら。</string>
<string name="yield_apy_boost_story_first_subtitle">初めて利息モードを有効にすると、最初の30日間は最大3倍の利回りを獲得できます。</string>
<string name="yield_apy_boost_story_first_title">初月APRボーナス</string>

View file

@ -91,6 +91,7 @@
<string name="add_custom_token_title">Adicionar token personalizado</string>
<string name="add_tokens_title">Gerenciar tokens</string>
<string name="addfunds_buy_row_description">Cartão de crédito ou conta bancária</string>
<string name="addfunds_fund_token">Adicionar token</string>
<string name="addfunds_receive_row_description">Compartilhe seu endereço ou código QR.</string>
<string name="addfunds_swap_row_description">Entre seus portfólios</string>
<string name="addfunds_you_receive_title">Você recebe</string>
@ -227,7 +228,7 @@
<string name="common_action_failed">%s fracassado</string>
<string name="common_activate">Ativar</string>
<string name="common_add">Adicionar</string>
<string name="common_add_funds">Adicionar fundos</string>
<string name="common_add_funds">Depositar</string>
<string name="common_add_to_portfolio">Adicionar ao portfólio</string>
<string name="common_add_token">Adicionar token</string>
<string name="common_add_tokens">Adicionar tokens</string>
@ -662,6 +663,11 @@
<string name="feedback_subject_support_tangem">Feedback Tangem</string>
<string name="feedback_subject_tx_failed">Não foi possível enviar uma transação.</string>
<string name="feedback_token_description_error">Erro na descrição da moeda</string>
<string name="force_update_banner_message">Atualize o aplicativo para a versão mais recente para garantir o funcionamento correto.</string>
<string name="force_update_banner_title">Atualização necessária</string>
<string name="force_update_button">Atualizar</string>
<string name="force_update_warning_message">Por favor, atualize o aplicativo para a versão mais recente para garantir o funcionamento correto.</string>
<string name="force_update_warning_title">Atualização necessária</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fundos insuficientes</string>
<string name="gasless_transaction_fee">Taxa de transação</string>
<string name="generic_error">Ocorreu um erro.</string>
@ -787,6 +793,8 @@
<string name="koinos_mana_level_description">A rede Koinos exige Mana para o pagamento das taxas de rede. Você tem %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Nível de mana</string>
<string name="main_add_and_manage_tokens">Adicionar e gerenciar</string>
<string name="main_add_funds_promo_description">Compre ou receba criptomoedas para começar a usar sua carteira.</string>
<string name="main_add_funds_promo_title">Adquira suas primeiras criptomoedas.</string>
<string name="main_empty_tokens_list_message">Para começar a rastrear seus criptoativos e transações, adicione tokens.</string>
<string name="main_manage_tokens">Gerenciar tokens</string>
<string name="main_qr_scan_hint">Leia o código QR para enviar fundos ou conectar-se a um aplicativo</string>
@ -1073,7 +1081,7 @@
<string name="onboarding_create_wallet_options_button_options">Outras opções</string>
<string name="onboarding_create_wallet_options_message">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.</string>
<string name="onboarding_create_wallet_options_title">Gere chaves de forma privada</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Ao continuar, você concorda com os termos. %s</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Ao continuar, você concorda com os termos.\n%s</string>
<string name="onboarding_done_body">Seu cartão está ativado e pronto para uso.</string>
<string name="onboarding_done_header">Sucesso!</string>
<string name="onboarding_done_wallet">Sua carteira está configurada e pronta para uso!</string>
@ -1210,9 +1218,7 @@
<string name="organize_tokens_title">Organizar tokens</string>
<string name="organize_tokens_ungroup">Desagrupar</string>
<string name="provider_name_support">%s suporte</string>
<string name="push_notification_settings_banner_button_grant_permission">Conceder permissão</string>
<string name="push_notification_settings_banner_description">As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo.</string>
<string name="push_notification_settings_banner_description_grant_permission">As notificações push estão ativadas, mas não funcionarão até que você conceda permissão.</string>
<string name="push_notification_settings_banner_title">Permitir notificações</string>
<string name="push_notification_settings_offers_updates_subtitle">Novidades sobre produtos, ofertas exclusivas e lembretes de atividades.</string>
<string name="push_notification_settings_offers_updates_title">Ofertas e atualizações</string>
@ -1684,11 +1690,13 @@
<string name="tangem_pay_freeze_card_failed">Não foi possível bloquear o cartão. Tente novamente mais tarde.</string>
<string name="tangem_pay_freeze_card_freeze">Congelar</string>
<string name="tangem_pay_freeze_card_success">Seu cartão está bloqueado.</string>
<string name="tangem_pay_freeze_card_unfreeze">Descongelar</string>
<string name="tangem_pay_get_help">Obtenha ajuda</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Razão: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Outro</string>
<string name="tangem_pay_pin_code_title">Código PIN</string>
<string name="tangem_pay_rooted_device_subtitle">Não é possível usar em dispositivos com root.</string>
<string name="tangem_pay_status_completed">Concluído</string>
<string name="tangem_pay_status_declined">Recusado</string>
@ -1697,15 +1705,15 @@
<string name="tangem_pay_terms_fees_limits">Termos, taxas e limites</string>
<string name="tangem_pay_terms_limits">Termos e Limites</string>
<string name="tangem_pay_transaction_declined_notification_text">O banco rejeitou esta solicitação de transação.</string>
<string name="tangem_pay_transaction_fee_notification_text">Essa taxa destina-se a cobrir os custos de processamento da sua transferência.</string>
<string name="tangem_pay_transaction_fee_notification_text">Uma taxa é cobrada de acordo com as tarifas de serviço</string>
<string name="tangem_pay_transaction_reversed_notification_text">A transação foi parcial ou totalmente revertida pelo comerciante.</string>
<string name="tangem_pay_unfreeze_card_alert_body">Continue usando seu dinheiro. Você pode congelar a qualquer momento.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Descongelar seu cartão?</string>
<string name="tangem_pay_unfreeze_card_failed">Não foi possível desbloquear o cartão. Tente novamente mais tarde.</string>
<string name="tangem_pay_unfreeze_card_success">Seu cartão foi desbloqueado.</string>
<string name="tangem_pay_withdrawal">Retirada</string>
<string name="tangempay_account_deactivated_message_subtitle">Isso foi feito devido a requisitos regulatórios. No entanto, saques ainda estão disponíveis.</string>
<string name="tangempay_account_deactivated_message_title">Seu cartão foi desativado</string>
<string name="tangempay_account_deactivated_message_subtitle">Para dúvidas sobre sua conta, dados ou histórico de transações, entre em contato com o suporte</string>
<string name="tangempay_account_deactivated_message_title">Sua conta foi encerrada</string>
<string name="tangempay_account_unable_to_use_rooted">Não é possível usar em dispositivos com root.</string>
<string name="tangempay_available_balance">Saldo disponível</string>
<string name="tangempay_cancel_kyc">Ocultar KYC da tela principal</string>
@ -1739,7 +1747,6 @@
<string name="tangempay_card_details_open_wallet_title">Adicionar cartão ao Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Adicionar cartão ao Apple Pay</string>
<string name="tangempay_card_details_pin_code">Código PIN</string>
<string name="tangempay_card_details_receive_description">Compartilhe seu endereço ou mostre o código QR.</string>
<string name="tangempay_card_details_receive_error_description">Problemas técnicos detectados. Tente novamente mais tarde ou entre em contato com o suporte.</string>
<string name="tangempay_card_details_receive_error_title">Receber indisponível agora</string>
<string name="tangempay_card_details_reissue_card">Substituir cartão</string>
@ -1748,7 +1755,6 @@
<string name="tangempay_card_details_rename_card_placeholder">Nome do cartão</string>
<string name="tangempay_card_details_reveal_text">Revelar</string>
<string name="tangempay_card_details_show_details">Mostrar detalhes</string>
<string name="tangempay_card_details_swap_description">Troque qualquer ativo da sua carteira por um cartão.</string>
<string name="tangempay_card_details_title">Detalhes do cartão</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Por favor, tente novamente mais tarde.</string>
<string name="tangempay_card_details_unfreeze_card">Descongelar cartão</string>
@ -1766,6 +1772,7 @@
<string name="tangempay_card_page_daily_limit_change">Mudar</string>
<string name="tangempay_card_page_daily_limit_current_limit">Limite atual</string>
<string name="tangempay_card_page_daily_limit_error_description">Não foi possível carregar seu limite diário. Tente novamente.</string>
<string name="tangempay_card_page_daily_limit_error_subtitle">Recarregue a página para tentar novamente.</string>
<string name="tangempay_card_page_daily_limit_error_title">Limite diário indisponível</string>
<string name="tangempay_card_page_daily_limit_success_description">Você pode alterar isso novamente quando quiser.</string>
<string name="tangempay_card_page_daily_limit_success_title">O limite diário está definido.</string>
@ -1827,7 +1834,7 @@
<string name="tangempay_onboarding_security_title">Privacidade incomparável</string>
<string name="tangempay_onboarding_setup_wallet_description">E vincule um cartão de pagamento a ele.</string>
<string name="tangempay_onboarding_setup_wallet_title">Vamos configurar uma carteira.</string>
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay gratuito em minutos.</string>
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay em minutos</string>
<string name="tangempay_pay_support">Suporte de Pay</string>
<string name="tangempay_payment_account">Conta de pagamento</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay sessão expirada</string>
@ -1851,7 +1858,6 @@
<string name="tangempay_sync_needed">Use o cartão ou anel para renovar a sessão</string>
<string name="tangempay_sync_needed_body">Use o cartão ou anel para renovar a sessão</string>
<string name="tangempay_sync_needed_button">Restaurar acesso</string>
<string name="tangempay_sync_needed_restore_access">Restaurar acesso</string>
<string name="tangempay_sync_needed_title">Tangem Pay sessão expirada</string>
<string name="tangempay_tangem_visa_card">Use USDC para pagamentos do dia a dia.</string>
<string name="tangempay_temporarily_unavailable">O serviço Tangem Pay está temporariamente inacessível.</string>
@ -1861,7 +1867,6 @@
<string name="tangempay_topup_swap_body">Troque qualquer ativo por USDC Polygon</string>
<string name="tangempay_topup_swap_title">Da sua Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC na rede Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Clique no botão abaixo para restaurar o acesso.</string>
<string name="tangempay_withdrawal_note_description">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</string>
<string name="tangempay_withdrawal_note_title">Observe</string>
<string name="tangempay_your_pin_code">Seu código PIN</string>

View file

@ -231,7 +231,7 @@
<string name="common_accounts">Аккаунты</string>
<string name="common_activate">Активировать</string>
<string name="common_add">Добавить</string>
<string name="common_add_funds">Добавить средств</string>
<string name="common_add_funds">Пополнить</string>
<string name="common_add_to_portfolio">Добавить в портфель</string>
<string name="common_add_token">Добавить токен</string>
<string name="common_add_tokens">Добавьте токены</string>
@ -410,6 +410,7 @@
<string name="common_select_action">Выберите действие</string>
<string name="common_sell">Продать</string>
<string name="common_send">Отправить</string>
<string name="common_send_colon">Отправка:</string>
<string name="common_send_tx_error">Не удалось отправить транзакцию</string>
<string name="common_server_unavailable">Сервер недоступен, повторите попытку позднее</string>
<string name="common_share">Поделиться</string>
@ -782,6 +783,8 @@
<string name="koinos_mana_level_description">Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Уровень маны</string>
<string name="main_add_and_manage_tokens">Добавить и управлять</string>
<string name="main_add_funds_promo_description">Купите криптовалюту или переведите её на свой кошелёк.</string>
<string name="main_add_funds_promo_title">Пополните кошелёк</string>
<string name="main_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_qr_scan_hint">Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению.</string>
@ -1684,15 +1687,15 @@
<string name="tangem_pay_terms_fees_limits">Тарифы и полные условия</string>
<string name="tangem_pay_terms_limits">Тарифы и лимиты</string>
<string name="tangem_pay_transaction_declined_notification_text">Банк отклонил транзакцию</string>
<string name="tangem_pay_transaction_fee_notification_text">Эта комиссия покрывает стоимость обработки вашего перевода.</string>
<string name="tangem_pay_transaction_fee_notification_text">Комиссия взимается в соответствии с тарифами обслуживания</string>
<string name="tangem_pay_transaction_reversed_notification_text">Транзакция частично или полностью возвращена продавцом</string>
<string name="tangem_pay_unfreeze_card_alert_body">Продолжайте пользоваться картой, заморозить всегда успеете</string>
<string name="tangem_pay_unfreeze_card_alert_title">Разморозить карту?</string>
<string name="tangem_pay_unfreeze_card_failed">Не удалось разморозить карту, попробуйте еще раз</string>
<string name="tangem_pay_unfreeze_card_success">Карта разморожена</string>
<string name="tangem_pay_withdrawal">Вывод средств</string>
<string name="tangempay_account_deactivated_message_subtitle">Это произошло из-за регуляторных требований. Вывод средств по-прежнему доступен.</string>
<string name="tangempay_account_deactivated_message_title">Карта была деактивирована</string>
<string name="tangempay_account_deactivated_message_subtitle">По вопросам данных или истории транзакций, обратитесь в поддержку</string>
<string name="tangempay_account_deactivated_message_title">Аккаунт закрыт</string>
<string name="tangempay_account_unable_to_use_rooted">Запрещено использовать на root-устройствах</string>
<string name="tangempay_available_balance">Баланс</string>
<string name="tangempay_cancel_kyc">Скрыть KYC с главной</string>
@ -1726,7 +1729,6 @@
<string name="tangempay_card_details_open_wallet_title">Добавьте карту в Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Добавить карту в Apple Pay</string>
<string name="tangempay_card_details_pin_code">ПИН-код</string>
<string name="tangempay_card_details_receive_description">Скопируйте свой адрес или покажите QR</string>
<string name="tangempay_card_details_receive_error_description">Техническая ошибка. Попробуйте позже или обратитесь в поддержку.</string>
<string name="tangempay_card_details_receive_error_title">Пополнение недоступно</string>
<string name="tangempay_card_details_reissue_card">Перевыпустить карту</string>
@ -1734,7 +1736,6 @@
<string name="tangempay_card_details_rename_card_invalid_title">Недопустимые символы</string>
<string name="tangempay_card_details_reveal_text">Показать</string>
<string name="tangempay_card_details_show_details">Реквизиты</string>
<string name="tangempay_card_details_swap_description">Пополните карту любым активом через обмен </string>
<string name="tangempay_card_details_title">Реквизиты</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Пожалуйста, попробуйте позже</string>
<string name="tangempay_card_details_unfreeze_card">Разморозить карту</string>
@ -1816,7 +1817,6 @@
<string name="tangempay_sync_needed">Используйте карту или кольцо для обновления сессии</string>
<string name="tangempay_sync_needed_body">Используйте карту или кольцо для обновления сессии</string>
<string name="tangempay_sync_needed_button">Обновить сессию</string>
<string name="tangempay_sync_needed_restore_access">Обновить сессию</string>
<string name="tangempay_sync_needed_title">Tangem Pay · Cессия истекла</string>
<string name="tangempay_tangem_visa_card">Оплачивайте ежедневные покупки в USDC</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay временно недоступен</string>
@ -1826,7 +1826,6 @@
<string name="tangempay_topup_swap_body">Обменяйте любой актив на USDC Polygon</string>
<string name="tangempay_topup_swap_title">Из вашего кошелька Tangem</string>
<string name="tangempay_usdc_on_polygon_network">USDC в сети Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Нажмите на кнопку ниже, чтобы восстановить доступ</string>
<string name="tangempay_withdrawal_note_description">При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок</string>
<string name="tangempay_withdrawal_note_title">Обратите внимание</string>
<string name="tangempay_your_pin_code">Ваш PIN-код</string>
@ -2071,6 +2070,7 @@
<string name="warning_express_notification_invalid_reserve_amount_title">Сумма получения не может быть менее %s</string>
<string name="warning_express_pair_unavailable_message">Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s)</string>
<string name="warning_express_pair_unavailable_title">Выбранная пара временно недоступна</string>
<string name="warning_express_providers_fca_warning_description">Для пользователей из Великобритании: некоторые провайдеры не авторизованы FCA Великобритании. Вам следует избегать взаимодействия с ними.</string>
<string name="warning_express_providers_fca_warning_title">Предупреждающий список FCA</string>
<string name="warning_express_refresh_required_title">Сервис временно недоступен</string>
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>

View file

@ -384,6 +384,7 @@
<string name="common_select_action">Оберіть дію</string>
<string name="common_sell">Продати</string>
<string name="common_send">Надіслати</string>
<string name="common_send_colon">Відправка:</string>
<string name="common_send_tx_error">Не вдалося надіслати транзакцію</string>
<string name="common_server_unavailable">Сервер недоступний, спробуйте пізніше</string>
<string name="common_share">Поширити</string>
@ -1604,15 +1605,15 @@
<string name="tangem_pay_terms_fees_limits">Умови, комісії та ліміти</string>
<string name="tangem_pay_terms_limits">Умови та обмеження</string>
<string name="tangem_pay_transaction_declined_notification_text">Банк відхилив цей запит на транзакцію.</string>
<string name="tangem_pay_transaction_fee_notification_text">Ця комісія покриває витрати на обробку вашого переказу.</string>
<string name="tangem_pay_transaction_fee_notification_text">Комісія стягується відповідно до тарифів обслуговування</string>
<string name="tangem_pay_transaction_reversed_notification_text">Транзакцію було частково або повністю скасовано продавцем</string>
<string name="tangem_pay_unfreeze_card_alert_body">Продовжуйте користуватися карткою. Заморозити можна в будь-який момент.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Розморозити картку?</string>
<string name="tangem_pay_unfreeze_card_failed">Не вдалося розморозити картку. Спробуйте пізніше.</string>
<string name="tangem_pay_unfreeze_card_success">Картку розморожено.</string>
<string name="tangem_pay_withdrawal">Виведення коштів</string>
<string name="tangempay_account_deactivated_message_subtitle">Це було зроблено відповідно до регуляторних вимог. Виведення коштів усе ще доступне.</string>
<string name="tangempay_account_deactivated_message_title">Вашу картку було деактивовано</string>
<string name="tangempay_account_deactivated_message_subtitle">З питань щодо даних або історії транзакцій зверніться до служби підтримки</string>
<string name="tangempay_account_deactivated_message_title">Ваш обліковий запис було закрито</string>
<string name="tangempay_account_unable_to_use_rooted">Заборонено використовувати на root-пристроях</string>
<string name="tangempay_available_balance">Баланс</string>
<string name="tangempay_cancel_kyc">Приховати KYC з головного екрана</string>
@ -1644,7 +1645,6 @@
<string name="tangempay_card_details_open_wallet_title">Додайте картку до Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Додайте свою картку в Apple Pay</string>
<string name="tangempay_card_details_pin_code">ПІН-код</string>
<string name="tangempay_card_details_receive_description">Поділіться своєю адресою або покажіть QR-код</string>
<string name="tangempay_card_details_receive_error_description">Виявлено технічні проблеми. Будь ласка, спробуйте пізніше або зверніться до служби підтримки.</string>
<string name="tangempay_card_details_receive_error_title">Поповнення наразі недоступне</string>
<string name="tangempay_card_details_reissue_card">Перевипустити картку</string>
@ -1652,7 +1652,6 @@
<string name="tangempay_card_details_rename_card_invalid_title">Неприпустимі символи</string>
<string name="tangempay_card_details_reveal_text">Показати</string>
<string name="tangempay_card_details_show_details">Показати деталі</string>
<string name="tangempay_card_details_swap_description">Обміняйте будь-який актив у вашому портфелі на картку</string>
<string name="tangempay_card_details_title">Реквізити картки</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Будь ласка, спробуйте пізніше</string>
<string name="tangempay_card_details_unfreeze_card">Розморозити картку</string>
@ -1713,7 +1712,7 @@
<string name="tangempay_onboarding_purchases_title">Платіть стільки, скільки бачите</string>
<string name="tangempay_onboarding_security_description">Буде створено окремий платіжний рахунок без розкриття ваших адрес та активів</string>
<string name="tangempay_onboarding_security_title">Неперевершена конфіденційність</string>
<string name="tangempay_onboarding_title">Отримайте безкоштовну картку Tangem Pay за лічені хвилини</string>
<string name="tangempay_onboarding_title">Отримайте картку Tangem Pay за лічені хвилини</string>
<string name="tangempay_payment_account">Платіжний акаунт</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay · Сесія закінчилася</string>
<string name="tangempay_pin_validation_error_message">Слабкий ПІН: не використовуйте повторів або послідовностей.</string>
@ -1735,7 +1734,6 @@
<string name="tangempay_sync_needed">Використайте картку або кільце для поновлення сесії</string>
<string name="tangempay_sync_needed_body">Використайте картку або кільце для поновлення сесії</string>
<string name="tangempay_sync_needed_button">Відновити доступ</string>
<string name="tangempay_sync_needed_restore_access">Відновити доступ</string>
<string name="tangempay_sync_needed_title">Tangem Pay · Сесія закінчилася</string>
<string name="tangempay_tangem_visa_card">Використовуйте USDC для щоденних платежів</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay тимчасово недоступний</string>
@ -1745,7 +1743,6 @@
<string name="tangempay_topup_swap_body">Обміняйте будь-який актив на USDC Polygon</string>
<string name="tangempay_topup_swap_title">З вашого Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC у Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Натисніть кнопку нижче, щоб відновити доступ</string>
<string name="tangempay_withdrawal_note_description">Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок.</string>
<string name="tangempay_withdrawal_note_title">Зверніть увагу</string>
<string name="tangempay_your_pin_code">Ваш PIN-код</string>

View file

@ -393,6 +393,7 @@
<string name="common_sending">发送中</string>
<string name="common_sent">发送</string>
<string name="common_server_unavailable">服务器不可用,请稍后再试。</string>
<string name="common_session_expired">会话已过期</string>
<string name="common_share">分享</string>
<string name="common_share_link">分享链接</string>
<string name="common_show_less">显示更少</string>
@ -1187,9 +1188,7 @@
<string name="organize_tokens_title">整理代币</string>
<string name="organize_tokens_ungroup">取消分组</string>
<string name="provider_name_support">%s 支持</string>
<string name="push_notification_settings_banner_button_grant_permission">授予权限</string>
<string name="push_notification_settings_banner_description">推送通知已启用,但需要您在设备设置中允许通知才能正常工作。</string>
<string name="push_notification_settings_banner_description_grant_permission">推送通知已启用,但需要您授予权限才能生效。</string>
<string name="push_notification_settings_banner_title">允许通知</string>
<string name="push_notification_settings_offers_updates_subtitle">产品资讯、独家优惠和活动提醒。</string>
<string name="push_notification_settings_offers_updates_title">优惠与更新</string>
@ -1671,15 +1670,15 @@
<string name="tangem_pay_terms_fees_limits">条款、费用和限制</string>
<string name="tangem_pay_terms_limits">条款和限制</string>
<string name="tangem_pay_transaction_declined_notification_text">银行拒绝了这项交易请求。</string>
<string name="tangem_pay_transaction_fee_notification_text">这笔费用用于支付您办理转账时的费用。</string>
<string name="tangem_pay_transaction_fee_notification_text">费用按服务费率收取</string>
<string name="tangem_pay_transaction_reversed_notification_text">商家部分或全部撤销了交易</string>
<string name="tangem_pay_unfreeze_card_alert_body">继续使用您的资金。您可以随时冻结资金。</string>
<string name="tangem_pay_unfreeze_card_alert_title">要解冻您的卡片?</string>
<string name="tangem_pay_unfreeze_card_failed">卡片解冻失败,请稍后再试。</string>
<string name="tangem_pay_unfreeze_card_success">您的卡片已解冻。</string>
<string name="tangem_pay_withdrawal">提款</string>
<string name="tangempay_account_deactivated_message_subtitle">这是根据监管要求执行的。不过,提现仍然可用。</string>
<string name="tangempay_account_deactivated_message_title">您的卡已停用</string>
<string name="tangempay_account_deactivated_message_subtitle">如需咨询账户、数据或交易记录,请联系支持团队</string>
<string name="tangempay_account_deactivated_message_title">您的账户已被关闭</string>
<string name="tangempay_account_unable_to_use_rooted">无法在已root的设备上使用</string>
<string name="tangempay_available_balance">可用余额</string>
<string name="tangempay_cancel_kyc">从主屏幕隐藏 KYC 页面</string>
@ -1713,7 +1712,6 @@
<string name="tangempay_card_details_open_wallet_title">将卡片添加到 Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">将卡片添加到 Apple Pay</string>
<string name="tangempay_card_details_pin_code">PIN码</string>
<string name="tangempay_card_details_receive_description">分享您的地址或出示二维码</string>
<string name="tangempay_card_details_receive_error_description">检测到技术问题。请稍后再试或联系技术支持。</string>
<string name="tangempay_card_details_receive_error_title">目前无法接收</string>
<string name="tangempay_card_details_reissue_card">重新发行卡片</string>
@ -1722,7 +1720,6 @@
<string name="tangempay_card_details_rename_card_placeholder">卡片名称</string>
<string name="tangempay_card_details_reveal_text">显示</string>
<string name="tangempay_card_details_show_details">显示详情</string>
<string name="tangempay_card_details_swap_description">将您投资组合中的任何资产互换到卡片</string>
<string name="tangempay_card_details_title">卡片详情</string>
<string name="tangempay_card_details_unable_to_rename_card_description">请稍后再试。</string>
<string name="tangempay_card_details_unfreeze_card">解冻卡片</string>
@ -1800,7 +1797,7 @@
<string name="tangempay_onboarding_security_title">无与伦比的隐私保护</string>
<string name="tangempay_onboarding_setup_wallet_description">并将其与支付卡关联。</string>
<string name="tangempay_onboarding_setup_wallet_title">我们将设置一个钱包。</string>
<string name="tangempay_onboarding_title">几分钟内即可获得免费的 Tangem Pay 卡</string>
<string name="tangempay_onboarding_title">立即获取你的 Tangem Pay 卡</string>
<string name="tangempay_pay_support">支付支持</string>
<string name="tangempay_payment_account">支付账户</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay 会话已过期</string>
@ -1824,7 +1821,6 @@
<string name="tangempay_sync_needed">用卡或戒指续期会话</string>
<string name="tangempay_sync_needed_body">用卡或戒指续期会话</string>
<string name="tangempay_sync_needed_button">恢复访问权限</string>
<string name="tangempay_sync_needed_restore_access">恢复访问权限</string>
<string name="tangempay_sync_needed_title">Tangem Pay 会话已过期</string>
<string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay暂时无法使用。</string>
@ -1834,7 +1830,6 @@
<string name="tangempay_topup_swap_body">將任何資產兌換為 USDC Polygon</string>
<string name="tangempay_topup_swap_title">從您的 Tangem 錢包</string>
<string name="tangempay_usdc_on_polygon_network">Polygon网络上的 USDC</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">点击下方按钮恢复访问权限</string>
<string name="tangempay_withdrawal_note_description">您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。</string>
<string name="tangempay_withdrawal_note_title">请注意</string>
<string name="tangempay_your_pin_code">您的PIN码</string>

View file

@ -337,15 +337,15 @@
<string name="tangem_pay_terms_fees_limits">條款、費用與限制</string>
<string name="tangem_pay_terms_limits">條款與限制</string>
<string name="tangem_pay_transaction_declined_notification_text">銀行拒絕了此交易請求。</string>
<string name="tangem_pay_transaction_fee_notification_text">此費用用於支付處理您轉帳的成本。</string>
<string name="tangem_pay_transaction_fee_notification_text">費用依服務費率收取</string>
<string name="tangem_pay_transaction_reversed_notification_text">該交易已被商家部分或全額撤銷</string>
<string name="tangem_pay_unfreeze_card_alert_body">繼續使用您的資金。您可以隨時凍結。</string>
<string name="tangem_pay_unfreeze_card_alert_title">解凍您的卡片?</string>
<string name="tangem_pay_unfreeze_card_failed">無法解凍卡片。請稍後再試。</string>
<string name="tangem_pay_unfreeze_card_success">您的卡片已解凍。</string>
<string name="tangem_pay_withdrawal">提現</string>
<string name="tangempay_account_deactivated_message_subtitle">这是根据监管要求执行的。不过,提现仍然可用。</string>
<string name="tangempay_account_deactivated_message_title">您的卡已停用</string>
<string name="tangempay_account_deactivated_message_subtitle">如需查詢帳戶、資料或交易記錄,請聯絡客服支援</string>
<string name="tangempay_account_deactivated_message_title">您的帳戶已被關閉</string>
<string name="tangempay_cancel_kyc">在主畫面隱藏身份驗證</string>
<string name="tangempay_card_details_add_funds">添加资金</string>
<string name="tangempay_card_details_add_funds_subtitle">充值选项</string>
@ -374,12 +374,10 @@
<string name="tangempay_card_details_open_wallet_step_5">全部完成!您的卡片已準備就緒。</string>
<string name="tangempay_card_details_open_wallet_title">將卡片添加到 Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">添加卡片到 Apple Pay</string>
<string name="tangempay_card_details_receive_description">分享您的地址或显示二维码</string>
<string name="tangempay_card_details_receive_error_title">暫時無法接收</string>
<string name="tangempay_card_details_reissue_card">重新发行卡片</string>
<string name="tangempay_card_details_reveal_text">显示</string>
<string name="tangempay_card_details_show_details">顯示詳情</string>
<string name="tangempay_card_details_swap_description">將您投資組合中的任何資產兌換成卡片</string>
<string name="tangempay_card_details_title">卡片详情</string>
<string name="tangempay_card_details_unfreeze_card">解凍卡片</string>
<string name="tangempay_card_details_withdraw">提现</string>
@ -420,7 +418,7 @@
<string name="tangempay_onboarding_purchases_title">所見即所付</string>
<string name="tangempay_onboarding_security_description">將創建單獨的支付帳戶,且不會透露您的地址和資產</string>
<string name="tangempay_onboarding_security_title">無與倫比的隱私</string>
<string name="tangempay_onboarding_title">在幾分鐘內獲得免費的 Tangem Pay 卡</string>
<string name="tangempay_onboarding_title">立即獲取你的 Tangem Pay 卡</string>
<string name="tangempay_payment_account">付款帳戶</string>
<string name="tangempay_payment_account_sync_needed">Tangem Pay 工作階段已過期</string>
<string name="tangempay_reissue_card_description">這將產生一組新的卡片資料。您的舊資料將停止使用。此操作無法復原。</string>
@ -439,7 +437,6 @@
<string name="tangempay_topup_receive_title">从其他钱包或交易所</string>
<string name="tangempay_topup_swap_body">将任何资产兑换为 USDC Polygon</string>
<string name="tangempay_topup_swap_title">从您的 Tangem 钱包</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">點擊下方按鈕以恢復存取權限</string>
<string name="tangempay_withdrawal_note_description">您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。</string>
<string name="tangempay_withdrawal_note_title">請注意</string>
<string name="tangempay_your_pin_code">您的PIN码</string>

View file

@ -664,6 +664,11 @@
<string name="feedback_subject_support_tangem">Tangem feedback</string>
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
<string name="feedback_token_description_error">Coin description error</string>
<string name="force_update_banner_message">Update the application to the latest version to ensure proper functionality</string>
<string name="force_update_banner_title">Update Needed</string>
<string name="force_update_button">Update</string>
<string name="force_update_warning_message">Please update the application to the latest version to ensure proper functionality.</string>
<string name="force_update_warning_title">Update Required</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Not enough funds</string>
<string name="gasless_transaction_fee">Transaction fee</string>
<string name="generic_error">An error occurred</string>
@ -789,8 +794,8 @@
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Mana level</string>
<string name="main_add_and_manage_tokens">Add &amp; Manage</string>
<string name="main_add_funds_promo_description">Deposit crypto or buy with card to get started</string>
<string name="main_add_funds_promo_title">Add funds to start earning and trading</string>
<string name="main_add_funds_promo_description">Buy or receive crypto to start using your wallet.</string>
<string name="main_add_funds_promo_title">Get your first crypto</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_qr_scan_hint">Scan QR code to send funds or connect to an app</string>
@ -1701,15 +1706,15 @@
<string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string>
<string name="tangem_pay_terms_limits">Terms and fees</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged in accordance with the service tariffs</string>
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
<string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_account_deactivated_message_subtitle">This was done due to regulatory requirements. Anyway withdrawals are still available.</string>
<string name="tangempay_account_deactivated_message_title">Your card was deactivated</string>
<string name="tangempay_account_deactivated_message_subtitle">For questions about account, data or transaction history, please contact support</string>
<string name="tangempay_account_deactivated_message_title">Your account has been closed</string>
<string name="tangempay_account_unable_to_use_rooted">Unable to use on rooted device</string>
<string name="tangempay_available_balance">Available balance</string>
<string name="tangempay_cancel_kyc">Hide KYC from main screen</string>
@ -1830,7 +1835,7 @@
<string name="tangempay_onboarding_security_title">Unrivaled privacy</string>
<string name="tangempay_onboarding_setup_wallet_description">And link a payment card to it</string>
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
<string name="tangempay_onboarding_title">Get your free Tangem Pay Card in minutes</string>
<string name="tangempay_onboarding_title">Get your Tangem Pay Card in minutes</string>
<string name="tangempay_pay_support">Pay Support</string>
<string name="tangempay_payment_account">Payment account</string>
<string name="tangempay_payment_account_sync_needed">Payment account session expired</string>

View file

@ -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)
},
)
}
}

View file

@ -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)

View file

@ -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<Network>): Map<Network, String> {
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<Network, String>()
for (network in networks) {
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue

View file

@ -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<UniversalError, CryptoCurrency> {
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)
}
}
}

View file

@ -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
@ -13,6 +12,7 @@ import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import javax.inject.Inject
import javax.inject.Singleton
@ -44,6 +44,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,
@ -62,6 +63,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(),
)
@ -94,6 +96,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,
@ -123,6 +126,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate,
)
null -> PaymentAccountStatusValue.Error.Unavailable
}

View file

@ -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.*
@ -21,20 +21,13 @@ 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.CloseTangemPayCardUseCase
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
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
@ -80,9 +73,7 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindTangemPayCryptoCurrencyFactory(
factory: DefaultTangemPayCryptoCurrencyFactory,
): TangemPayCryptoCurrencyFactory
fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory
@Binds
@Singleton

View file

@ -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
}
}

View file

@ -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.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
@ -34,6 +37,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
@ -49,6 +53,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val eligibilityManager: TangemPayEligibilityManager,
private val reissueCardRepository: TangemPayReissueCardRepository,
private val singleQuoteSupplier: SingleQuoteStatusSupplier,
private val closeCardRepository: TangemPayCloseCardRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : PaymentAccountStatusFetcher {
@ -263,6 +268,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
@ -285,12 +293,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)
@ -302,6 +312,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
productInstance: CustomerInfo.ProductInstance,
cardInfo: CustomerInfo.CardInfo,
customerId: String,
fiatRate: BigDecimal?,
): PaymentAccountStatusValue {
val cardId = productInstance.cardId
val cardState = getCardState(cardId, userWalletId)
@ -316,6 +327,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
cryptoBalance = cardInfo.cryptoBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal,
cryptoCurrency = cryptoCurrency,
fiatRate = fiatRate,
cards = listOf(
TangemPayCard(
id = cardId,

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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<TangemPayCard>,
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 }

View file

@ -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<CryptoCurrency>): Either<Throwable, Unit> {
suspend fun fetchQuotes(rawCurrencyIds: Set<CryptoCurrency.RawID>): Either<Throwable, Unit> {
return multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrencyIds, appCurrencyId = null),
)
}

View file

@ -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))
}
}

View file

@ -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<UniversalError, CryptoCurrency>
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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(

View file

@ -316,9 +316,9 @@ class YieldSupplyEstimateEnterFeeUseCaseTest {
val deployFee = txs.first().fee as Fee.Ethereum.EIP1559
val approveFee = txs[1].fee as Fee.Ethereum.EIP1559
val enterFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_200))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_400))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_600))
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_400))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_800))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(4_200))
}
@Test
@ -355,9 +355,9 @@ class YieldSupplyEstimateEnterFeeUseCaseTest {
val deployFee = txs.first().fee as Fee.Ethereum.Legacy
val approveFee = txs[1].fee as Fee.Ethereum.Legacy
val enterFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_200))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_400))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_600))
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_400))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_800))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(4_200))
}
private fun getDeployTx() = uncompiled(

View file

@ -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,

View file

@ -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"),
)
}

View file

@ -129,5 +129,6 @@ interface SwapInteractor {
amount: SwapAmount,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee>
}

View file

@ -1032,6 +1032,7 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee> = either {
if (amount.value.signum() == 0) {
raise(GetFeeError.UnknownError)
@ -1048,6 +1049,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromStatus = fromStatus,
amount = amount,
selectedFeeToken = selectedFeeToken,
isGasless = isGasless,
)
}
}
@ -1145,12 +1147,14 @@ internal class SwapInteractorImpl @Inject constructor(
fromStatus: SwapCurrencyStatus,
amount: SwapAmount,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee> {
return cexSwapFeeCalculator.calculate(
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = amount.value,
selectedFeeToken = selectedFeeToken,
isGasless = isGasless,
).fold(
ifLeft = { it.left() },
ifRight = { cexFeeResult ->

View file

@ -45,40 +45,51 @@ class CexSwapFeeCalculator(
fromSwapCurrencyStatus: SwapCurrencyStatus,
amount: BigDecimal,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, CexFeeResult> = 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)

View file

@ -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<TransactionFeeExtended>(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<TransactionFeeExtended>(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<Fee.Common>(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()

View file

@ -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) {

View file

@ -8,13 +8,17 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.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.domain.models.network.Network
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
private const val SWAP_CATEGORY = "Swap"
@ -38,6 +42,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(
@ -76,9 +113,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(
@ -249,6 +294,11 @@ sealed class SwapEvents(
),
)
class FastAmountInput(percent: PredefinedPercentAmount) : SwapEvents(
event = "Fast amount input",
params = mapOf("Percentage" to percent.toAnalyticsValue()),
)
class TransferModeSwitched(
fromCurrency: CryptoCurrency?,
toCurrency: CryptoCurrency?,
@ -290,4 +340,11 @@ sealed class SwapEvents(
"Network fee" to feeNetwork.name,
),
), AppsFlyerIncludedEvent
}
private fun PredefinedPercentAmount.toAnalyticsValue(): String = when (this) {
PredefinedPercentAmount.PERCENT_25 -> "25"
PredefinedPercentAmount.PERCENT_50 -> "50"
PredefinedPercentAmount.PERCENT_75 -> "75"
PredefinedPercentAmount.MAX -> "Max"
}

View file

@ -75,6 +75,8 @@ internal object SwapProviderStateBuilder {
permissionState: PermissionDataState,
pricesLowerBest: Map<String, Float>,
selectionType: ProviderState.SelectionType,
isBestRate: Boolean = false,
isNeedBestRateBadge: Boolean = false,
needApplyFCARestrictions: Boolean,
onProviderClick: (String) -> Unit,
onApprovalSelectClick: (SwapProvider) -> Unit = {},
@ -85,6 +87,8 @@ internal object SwapProviderStateBuilder {
provider = provider,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = permissionState,
isBestRate = isBestRate,
isNeedBestRateBadge = isNeedBestRateBadge,
),
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[provider.providerId]

View file

@ -351,7 +351,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))
}
}
@ -1665,6 +1667,7 @@ internal class SwapModel @Inject constructor(
}
private fun onPredefinedPercentSelected(percent: PredefinedPercentAmount) {
analyticsEventHandler.send(SwapEvents.FastAmountInput(percent))
if (percent == PredefinedPercentAmount.MAX) {
onMaxAmountClicked()
return
@ -1807,6 +1810,7 @@ internal class SwapModel @Inject constructor(
SwapEvents.ButtonSwapClicked(
sendToken = sendTokenSymbol,
receiveToken = receiveTokenSymbol,
swapUIMode = uiState.swapUIMode,
),
)
}
@ -1854,12 +1858,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 ->
@ -1924,15 +1931,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?,
@ -2309,7 +2335,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
@ -2365,6 +2392,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

View file

@ -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<PredefinedPercentButtonUM> = persistentListOf(),
val tosState: TosState? = null,
val swapUIMode: SwapUIMode = SwapUIMode.Detailed,
val shouldShowAbMenu: Boolean = false,
val isPredefinedButtonsEnabled: Boolean = false,
val transferFooter: TextReference? = null,
@ -43,9 +43,9 @@ 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 = {},
)
@Immutable

View file

@ -32,4 +32,5 @@ internal data class UiActions(
val onLinkClick: (String) -> Unit,
val onReceiveCardWarningClick: () -> Unit,
val onSwapUIModeChange: (SwapUIMode) -> Unit,
val onSwapTypeMenuOpened: () -> Unit,
)

View file

@ -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)
@ -143,6 +176,18 @@ private class SimpleProviderPreview : PreviewParameterProvider<ProviderState> {
approvalSettings = ProviderState.ApprovalSettings.Empty,
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",

View file

@ -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.onApproveClick,
onSelectTokenClick = actions.onSelectTokenClick,
@ -108,8 +109,8 @@ internal class StateBuilder(
isInsufficientFunds = false,
swapUIMode = swapUIMode,
onSwapUIModeChange = actions.onSwapUIModeChange,
onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened,
shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled,
isPredefinedButtonsEnabled = swapFeatureToggles.isSwapPredefinedButtonsEnabled,
)
}
@ -141,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,
@ -214,6 +219,7 @@ internal class StateBuilder(
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
priceImpact = PriceImpact.Empty,
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency),
transferFooter = null,
)
}
@ -249,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,
@ -445,6 +455,7 @@ internal class StateBuilder(
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
priceImpact = PriceImpact.Empty,
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency),
)
}
@ -575,6 +586,7 @@ internal class StateBuilder(
priceImpact = priceImpact,
tosState = createTosState(swapProvider),
shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency),
predefinedButtons = createPredefinedButtons(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency),
)
}
@ -610,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<PredefinedPercentButtonUM> {
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 ->
@ -1025,6 +1068,8 @@ internal class StateBuilder(
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
needApplyFCARestrictions: Boolean,
bestRatedProviderId: String,
isNeedBestRateBadge: Boolean,
onDismiss: () -> Unit,
): SwapStateHolder {
val availableProvidersStates = providersStates.entries
@ -1034,6 +1079,8 @@ internal class StateBuilder(
onProviderSelect = actions.onProviderSelect,
needApplyFCARestrictions = needApplyFCARestrictions,
onApprovalSelectClick = actions.onApproveTypeSelect,
bestRatedProviderId = bestRatedProviderId,
isNeedBestRateBadge = isNeedBestRateBadge,
)
}
.sortedWith(ProviderPercentDiffComparator)
@ -1139,6 +1186,8 @@ internal class StateBuilder(
onProviderSelect: (String) -> Unit,
onApprovalSelectClick: (SwapProvider) -> Unit,
needApplyFCARestrictions: Boolean,
bestRatedProviderId: String,
isNeedBestRateBadge: Boolean,
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
@ -1151,6 +1200,8 @@ internal class StateBuilder(
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = needApplyFCARestrictions,
isBestRate = bestRatedProviderId == provider.providerId && !state.priceImpact.shouldShowWarning(),
isNeedBestRateBadge = isNeedBestRateBadge,
onProviderClick = onProviderSelect,
onApprovalSelectClick = onApprovalSelectClick,
)

View file

@ -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
},

View file

@ -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(

View file

@ -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,
)
}
}

View file

@ -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,
)

View file

@ -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 {

View file

@ -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<CryptoCurrency> {
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<String, String>,
@ -479,6 +628,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest {
getUserWalletUseCase = getUserWalletUseCase,
walletBalanceFetcher = walletBalanceFetcher,
singleAccountListSupplier = singleAccountListSupplier,
singleAccountListFetcher = singleAccountListFetcher,
getSelectedWalletSyncUseCase = getSelectedWalletSync,
)
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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"),
),
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -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())
}

View file

@ -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

View file

@ -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)
}
}