Updated on 2026-08-14
This commit is contained in:
commit
16ed712a09
77 changed files with 1429 additions and 621 deletions
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 d’avertissement 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 & 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 n’importe quel token, même s’il 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
@ -129,5 +129,6 @@ interface SwapInteractor {
|
|||
amount: SwapAmount,
|
||||
swapData: SwapDataModel?,
|
||||
selectedFeeToken: CryptoCurrencyStatus?,
|
||||
isGasless: Boolean,
|
||||
): Either<GetFeeError, SwapFee>
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ internal data class UiActions(
|
|||
val onLinkClick: (String) -> Unit,
|
||||
val onReceiveCardWarningClick: () -> Unit,
|
||||
val onSwapUIModeChange: (SwapUIMode) -> Unit,
|
||||
val onSwapTypeMenuOpened: () -> Unit,
|
||||
)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue