Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 13:57:57 +03:00
commit f3dfb262c2
527 changed files with 12898 additions and 3301 deletions

View file

@ -161,7 +161,7 @@ dependencies {
implementation(projects.domain.hotWallet)
implementation(projects.domain.news)
implementation(projects.domain.earn)
implementation(projects.domain.tokensync)
implementation(projects.domain.assetsdiscovery)
implementation(projects.domain.search)
implementation(projects.common)
@ -192,7 +192,7 @@ dependencies {
implementation(projects.data.common)
implementation(projects.data.settings)
implementation(projects.data.tokens)
implementation(projects.data.tokensync)
implementation(projects.data.assetsdiscovery)
implementation(projects.data.txhistory)
implementation(projects.data.wallets)
implementation(projects.data.analytics)
@ -274,6 +274,8 @@ dependencies {
implementation(projects.features.nft.impl)
implementation(projects.features.walletconnect.api)
implementation(projects.features.walletconnect.impl)
implementation(projects.features.commonFeatures.api)
implementation(projects.features.commonFeatures.impl)
implementation(projects.features.usedesk.api)
implementation(projects.features.usedesk.impl)
implementation(projects.features.hotWallet.api)

View file

@ -8,6 +8,8 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport
import com.kaspersky.components.composesupport.config.addComposeSupport
@ -20,13 +22,14 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import io.qameta.allure.kotlin.Allure
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@ -58,6 +61,12 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var promoRepository: PromoRepository
@Inject
lateinit var walletManagersStore: WalletManagersStore
@Inject
lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -72,7 +81,11 @@ abstract class BaseTestCase : TestCase(
private val semanticTreePrinterRule = object : TestWatcher() {
override fun failed(e: Throwable?, description: Description?) {
runCatching { printAllRoots() }
runCatching {
runBlocking {
withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() }
}
}
}
}
@ -174,5 +187,6 @@ abstract class BaseTestCase : TestCase(
private companion object {
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L
}
}

View file

@ -35,6 +35,7 @@ object TestConstants {
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L
const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN"
@ -43,4 +44,14 @@ object TestConstants {
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
"hawk when"
const val SEED_PHRASE_18 = "crush idle include refuse expose kiss slot budget uphold when dinner certain holiday " +
"slow word armor butter suffer"
const val SEED_PHRASE_21 = "employ space oval venue wash clog zebra cover icon wash assist word debris inform " +
"cable meadow add game meat rigid pride"
const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " +
"bread much nature basic fun iron benefit egg error prosper"
}

View file

@ -0,0 +1,88 @@
package com.tangem.common.utils
import com.tangem.utils.logging.TangemLogger
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertTrue
/**
* Compares addresses from the app (clipboard JSON) with reference addresses from the QA tools API.
*
* Both JSON arrays contain objects with fields: blockchain, derivationPath, token (nullable), addresses (array).
* Comparison normalizes and sorts both arrays before diffing.
*/
object AddressComparisonHelper {
fun compareAddresses(appJson: String, apiJson: String) {
val appEntries = parseAndNormalize(appJson)
val apiEntries = parseAndNormalize(apiJson)
val missingInApp = apiEntries - appEntries.toSet()
val extraInApp = appEntries - apiEntries.toSet()
if (missingInApp.isEmpty() && extraInApp.isEmpty()) {
TangemLogger.i("Address comparison passed: ${appEntries.size} entries match")
return
}
val report = buildString {
appendLine("Address comparison FAILED")
if (missingInApp.isNotEmpty()) {
appendLine("\nMissing in app (expected from API but not found):")
missingInApp.forEach { appendLine(" - $it") }
}
if (extraInApp.isNotEmpty()) {
appendLine("\nExtra in app (found in app but not in API):")
extraInApp.forEach { appendLine(" - $it") }
}
appendLine("\nApp entries: ${appEntries.size}, API entries: ${apiEntries.size}")
}
TangemLogger.e(report)
assertTrue(report, false)
}
private fun parseAndNormalize(json: String): List<AddressEntry> {
val array = JSONArray(json)
val entries = mutableListOf<AddressEntry>()
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
entries.add(
AddressEntry(
blockchain = normalizeBlockchainName(obj.getString("blockchain")),
derivationPath = obj.getString("derivationPath").trim(),
token = obj.optString("token", null)?.trim()?.lowercase(),
addresses = parseAddresses(obj).sorted(),
),
)
}
return entries.sortedWith(
compareBy<AddressEntry> { it.blockchain }
.thenBy { it.derivationPath }
.thenBy { it.token },
)
}
private val blockchainNameOverrides = mapOf(
"chia network" to "chia",
)
private fun normalizeBlockchainName(name: String): String {
val normalized = name.trim().lowercase()
return blockchainNameOverrides[normalized] ?: normalized
}
private fun parseAddresses(obj: JSONObject): List<String> {
val addressesArray = obj.getJSONArray("addresses")
return (0 until addressesArray.length()).map { addressesArray.getString(it) }
}
private data class AddressEntry(
val blockchain: String,
val derivationPath: String,
val token: String?,
val addresses: List<String>,
)
}

View file

@ -16,10 +16,10 @@ fun getWcUri(
TangemLogger.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
.readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа
.writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи
.callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.callTimeout(90, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
@ -58,6 +58,59 @@ fun getWcUri(
}
}
fun getAddressesFromApi(
seedKey: String,
baseUrl: String = "[REDACTED_ENV_URL]",
): String? {
TangemLogger.i("Getting addresses for seed key: $seedKey")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.callTimeout(90, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url("$baseUrl/addresses")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
TangemLogger.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
val contentType = response.header("Content-Type") ?: ""
if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) {
TangemLogger.e("Unexpected response (not JSON), Content-Type: $contentType, body: $body")
return null
}
val jsonObject = JSONObject(body)
val data = jsonObject.optJSONObject("data") ?: jsonObject
val seedData = data.optJSONArray(seedKey)
if (seedData != null) {
TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries")
seedData.toString()
} else {
TangemLogger.e("No data found for seed key: $seedKey")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
TangemLogger.e("Error getting addresses", e)
null
}
}
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {

View file

@ -0,0 +1,122 @@
package com.tangem.scenarios
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.utils.AddressComparisonHelper
import com.tangem.common.utils.getClipboardText
import com.tangem.screens.onMainScreen
import com.tangem.screens.onTesterMenuScreen
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.logging.TangemLogger
import io.qameta.allure.kotlin.Allure.step
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertNotNull
private const val WALLET_MANAGERS_SETTLE_MS = 5_000L
private const val WALLET_MANAGERS_POLL_INTERVAL_MS = 500L
fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) {
var appAddressesJson: String? = null
step("Open 'Main Screen' with existing hot wallet") {
openMainScreenWithExistingHotWallet(seedPhrase)
}
step("Assert wallet balance = '$DASH_SIGN'") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
runCatching { onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } }.isSuccess
}
}
step("Assert 'Organize tokens' button is enabled") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
runCatching { onMainScreen { organizeTokensButton().assertIsEnabled() } }.isSuccess
}
}
step("Wait for all wallet managers to initialize") {
awaitWalletManagersStabilized()
}
step("Open tester menu") {
openTesterMenu()
}
step("Click on 'Addresses info' button") {
onTesterMenuScreen { addressesInfoButton.performClick() }
}
step("Click on 'JSON' tab") {
onTesterMenuScreen { jsonTab.performClick() }
}
step("Click on 'Copy' button") {
onTesterMenuScreen { copyButton.performClick() }
}
step("Get addresses JSON from clipboard") {
appAddressesJson = getClipboardText(ApplicationProvider.getApplicationContext())
assertNotNull("Clipboard is empty after copying addresses", appAddressesJson)
}
step("Compare app addresses with API reference") {
AddressComparisonHelper.compareAddresses(
appJson = requireNotNull(appAddressesJson),
apiJson = apiAddressesJson,
)
}
}
private const val TESTER_MENU_MAX_ATTEMPTS = 3
/**
* Presses 'Volume Down' twice to open tester menu.
* Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if the menu doesn't appear.
*/
private fun BaseTestCase.openTesterMenu() {
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
waitForIdle()
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
val opened = runCatching {
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
}.isSuccess
if (opened) {
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
return
}
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
}
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
}
/**
* Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS].
*
* Uses [getAllSync] with a polling interval instead of Flow, because the Flow only emits on changes
* if the count stabilizes, there would be no new emission to check the settle timeout against.
*/
private fun BaseTestCase.awaitWalletManagersStabilized() {
val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
?: error("No selected wallet found")
var lastSize = -1
var stableStart = System.currentTimeMillis()
runBlocking {
withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
while (true) {
val currentSize = walletManagersStore.getAllSync(walletId).size
val now = System.currentTimeMillis()
if (currentSize != lastSize) {
TangemLogger.i("Wallet managers count: $currentSize (was $lastSize)")
lastSize = currentSize
stableStart = now
} else if (now - stableStart >= WALLET_MANAGERS_SETTLE_MS) {
TangemLogger.i("Wallet managers stabilized at $currentSize entries")
return@withTimeout
}
delay(WALLET_MANAGERS_POLL_INTERVAL_MS)
}
}
}
}

View file

@ -15,11 +15,10 @@ fun BaseTestCase.scanCard(
mockContent: MockContent? = null,
isTwinsCard: Boolean = false,
) {
if (productType != null) {
MockProvider.setMocks(productType)
}
if (mockContent != null) {
MockProvider.setMocks(mockContent)
when {
mockContent != null -> MockProvider.setMocks(mockContent)
productType != null -> MockProvider.setMocks(productType)
else -> MockProvider.setMocks(ProductType.Wallet)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
@ -60,6 +59,57 @@ fun BaseTestCase.openMainScreen(
}
}
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Start with Mobile Wallet' button") {
onCreateWalletStartScreen { startWithMobileWalletButton.performClick() }
}
step("Click on 'Import existing wallet' button") {
onCreateMobileWalletScreen { importExistingWalletButton.performClick() }
}
step("Click on 'Phrase text field'") {
onImportWalletScreen { phraseTextField.performClick() }
}
step("Type seed phrase in 'Phrase text field'") {
onImportWalletScreen { phraseTextField.performTextReplacement(seedPhrase) }
}
step("Click on 'Import' button") {
onImportWalletScreen {
importButton.assertIsEnabled()
importButton.performClick()
}
}
step("Click on 'Continue' button") {
onImportWalletScreen {
continueButton.assertIsEnabled()
continueButton.performClick()
}
}
step("Click on 'Skip' button") {
onImportWalletScreen { skipButton.performClick() }
}
step("Click on 'Skip anyway' dialog button") {
onDialog { skipAnywayButton.performClick() }
}
step("Click on 'Finish' button") {
onImportWalletScreen {
finishButton.assertIsEnabled()
finishButton.performClick()
}
}
step("Assert 'Main' screen is displayed") {
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Dismiss Market Tooltip by clicking close button") {
onMarketsTooltipScreen { closeButton.clickWithAssertion() }
}
}
fun BaseTestCase.synchronizeAddresses(
balance: String? = null,
isBalanceAvailable: Boolean = true

View file

@ -0,0 +1,23 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class CreateMobileWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CreateMobileWalletPageObject>(semanticsProvider = semanticsProvider) {
val importExistingWalletButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.hw_import_existing_wallet))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCreateMobileWalletScreen(function: CreateMobileWalletPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,11 @@ class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsPr
hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card))
useUnmergedTree = true
}
val startWithMobileWalletButton: KNode = child {
hasText(getResourceString(OnboardingImplR.string.welcome_create_wallet_mobile_title))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) =

View file

@ -64,6 +64,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_change))
}
val skipAnywayButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.access_code_alert_skip_ok))
}
}
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =

View file

@ -0,0 +1,54 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.ImportWalletScreenTestTags
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 io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class ImportWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ImportWalletPageObject>(semanticsProvider = semanticsProvider) {
val phraseTextField: KNode = child {
hasTestTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD)
useUnmergedTree = true
}
val passphraseTextField: KNode = child {
hasTestTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD)
useUnmergedTree = true
}
val importButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_import)))
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_continue)))
useUnmergedTree = true
}
val skipButton: KNode = child {
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
hasText(getResourceString(R.string.common_skip))
useUnmergedTree = true
}
val finishButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_finish)))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onImportWalletScreen(function: ImportWalletPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,39 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.feature.tester.impl.R as TesterImplR
import com.tangem.core.ui.test.BaseButtonTestTags
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 io.github.kakaocup.kakao.common.utilities.getResourceString
class TesterMenuPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TesterMenuPageObject>(semanticsProvider = semanticsProvider) {
val backButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val addressesInfoButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(TesterImplR.string.addresses_info))
useUnmergedTree = true
}
val jsonTab: KNode = child {
hasText("JSON")
useUnmergedTree = true
}
val copyButton: KNode = child {
hasContentDescription("Copy")
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTesterMenuScreen(function: TesterMenuPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,86 @@
package com.tangem.tests.hotWallet
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.SEED_PHRASE_12
import com.tangem.common.constants.TestConstants.SEED_PHRASE_15
import com.tangem.common.constants.TestConstants.SEED_PHRASE_18
import com.tangem.common.constants.TestConstants.SEED_PHRASE_21
import com.tangem.common.constants.TestConstants.SEED_PHRASE_24
import com.tangem.common.utils.checkServiceHealth
import com.tangem.common.utils.getAddressesFromApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.verifyAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertNotNull
import org.junit.Test
@HiltAndroidTest
class AddressesTest : BaseTestCase() {
private var apiAddressesJson: String? = null
private fun setupAddressTestHooks(seedKey: String) = setupHooks(
additionalBeforeAppLaunchSection = {
val status = checkServiceHealth()
assertNotNull("QA tools service is unreachable ([REDACTED_ENV_URL]", status)
apiAddressesJson = getAddressesFromApi(seedKey)
assertNotNull("Failed to fetch reference addresses for '$seedKey'", apiAddressesJson)
},
)
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("1792")
@DisplayName("Hot wallet: auto derivation addresses for seed 12")
@Test
fun seed12AddressesTest() {
setupAddressTestHooks("twelve").run {
verifyAddresses(SEED_PHRASE_12, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5106")
@DisplayName("Hot wallet: auto derivation addresses for seed 15")
@Test
fun seed15AddressesTest() {
setupAddressTestHooks("fifteen").run {
verifyAddresses(SEED_PHRASE_15, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5107")
@DisplayName("Hot wallet: auto derivation addresses for seed 18")
@Test
fun seed18AddressesTest() {
setupAddressTestHooks("eighteen").run {
verifyAddresses(SEED_PHRASE_18, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5108")
@DisplayName("Hot wallet: auto derivation addresses for seed 21")
@Test
fun seed21AddressesTest() {
setupAddressTestHooks("twenty_one").run {
verifyAddresses(SEED_PHRASE_21, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5109")
@DisplayName("Hot wallet: auto derivation addresses for seed 24")
@Test
fun seed24AddressesTest() {
setupAddressTestHooks("twenty_four").run {
verifyAddresses(SEED_PHRASE_24, requireNotNull(apiAddressesJson))
}
}
}

View file

@ -0,0 +1,151 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class CardanoWarningsTest : BaseTestCase() {
private val tokenName = "Cardano"
private val minAmount = "ADA 1.00"
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
private val invalidAmountMessage =
getResourceString(R.string.send_notification_invalid_minimum_amount_text, minAmount, minAmount)
@AllureId("4204")
@DisplayName("Warnings: check warning, when remains less than 1 ADA")
@Test
fun afterTransactionRemainsLessThanMinimumAmountTest() {
val sendAmount = "19"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' warning is displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = true
)
}
}
}
@AllureId("4207")
@DisplayName("Warnings: check warning, when amount more than 1 ADA")
@Test
fun transactionAmountMoreThanOneTest() {
val sendAmount = "2.5"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' warning is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
@AllureId("4210")
@DisplayName("Warnings: check warning, when remains more than 1 ADA")
@Test
fun afterTransactionRemainsMoreThanMinimumAmountTest() {
val sendAmount = "18"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' warning is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -10,7 +9,6 @@ fun appReducer(action: Action, state: AppState): AppState {
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@ -12,7 +8,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val detailsState: DetailsState = DetailsState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -20,12 +15,9 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.Action
@ -8,10 +7,5 @@ sealed class GlobalAction : Action {
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: AppCurrency) : GlobalAction()
}
data class IsSignWithRing(val isSignWithRing: Boolean) : GlobalAction()
}

View file

@ -1,43 +0,0 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
object GlobalMiddleware {
val handler = globalMiddlewareHandler
}
private val globalMiddlewareHandler: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
handleAction(action)
nextDispatch(action)
}
}
}
private fun handleAction(action: Action) {
when (action) {
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
}
}
private fun restoreAppCurrency() {
scope.launch {
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
.getSelectedAppCurrency()
.firstOrNull()
?: AppCurrency.Default
store.dispatchWithMain(GlobalAction.RestoreAppCurrency.Success(currency))
}
}

View file

@ -13,13 +13,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.SaveScanResponse -> {
globalState.copy(scanResponse = action.scanResponse)
}
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.RestoreAppCurrency.Success -> {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
else -> globalState
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapWalletManager
import org.rekotlin.StateType
@ -9,7 +8,6 @@ data class GlobalState(
@Deprecated("Use scan response from selected user wallet")
val scanResponse: ScanResponse? = null,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val appCurrency: AppCurrency = AppCurrency.Default,
val isLastSignWithRing: Boolean = false,
) : StateType

View file

@ -1,79 +0,0 @@
package com.tangem.tap.common.redux.legacy
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.LegacyAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
internal object LegacyMiddleware {
private val prepareDetailsScreenJobHolder = JobHolder()
val legacyMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is LegacyAction.PrepareDetailsScreen -> {
selectedUserWallet()
.distinctUntilChanged { old, new ->
if (old is UserWallet.Cold && new is UserWallet.Cold) {
old.walletId == new.walletId &&
old.scanResponse == new.scanResponse
} else {
old.walletId == new.walletId
}
}
.onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState()
store.dispatchWithMain(
DetailsAction.PrepareScreen(
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
initializedAppSettingsState = initializedAppSettingsStateContent,
),
)
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
.saveIn(prepareDetailsScreenJobHolder)
}
}
next(action)
}
}
}
private fun selectedUserWallet(): Flow<UserWallet> {
return store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
}
/**
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
* previously it was initialized in runBlocking and blocked details screen
*/
private suspend fun initializeAppSettingsState(): AppSettingsState {
return AppSettingsState(
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT,
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
hasSecuredWallets = store.inject(DaggerGraphState::userWalletsListRepository).hasSecuredWallets(),
)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AssetsDiscoveryDomainModule {
@Provides
@Singleton
fun provideObserveAssetsDiscoveryUseCase(
assetsDiscoveryRepository: AssetsDiscoveryRepository,
): ObserveAssetsDiscoveryUseCase {
return ObserveAssetsDiscoveryUseCase(
assetsDiscoveryRepository = assetsDiscoveryRepository,
)
}
@Provides
@Singleton
fun provideAcknowledgeAssetsDiscoveryCompletionUseCase(
assetsDiscoveryRepository: AssetsDiscoveryRepository,
): AcknowledgeAssetsDiscoveryCompletionUseCase {
return AcknowledgeAssetsDiscoveryCompletionUseCase(
assetsDiscoveryRepository = assetsDiscoveryRepository,
)
}
@Provides
@Singleton
fun provideStartAssetsDiscoveryUseCase(
assetsDiscoveryRepository: AssetsDiscoveryRepository,
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
appCoroutineScope: AppCoroutineScope,
): StartAssetsDiscoveryUseCase {
return StartAssetsDiscoveryUseCase(
assetsDiscoveryRepository = assetsDiscoveryRepository,
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
appCoroutineScope = appCoroutineScope,
)
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.tap.di.domain
import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object DynamicAddressesDomainModule {
@Provides
@Singleton
fun provideEnableDynamicAddressesUseCase(
dynamicAddressesRepository: DynamicAddressesRepository,
): EnableDynamicAddressesUseCase {
return EnableDynamicAddressesUseCase(dynamicAddressesRepository)
}
@Provides
@Singleton
fun provideDisableDynamicAddressesUseCase(
dynamicAddressesRepository: DynamicAddressesRepository,
): DisableDynamicAddressesUseCase {
return DisableDynamicAddressesUseCase(dynamicAddressesRepository)
}
@Provides
@Singleton
fun provideGetDynamicAddressesStatusUseCase(
dynamicAddressesRepository: DynamicAddressesRepository,
): GetDynamicAddressesStatusUseCase {
return GetDynamicAddressesStatusUseCase(dynamicAddressesRepository)
}
@Provides
@Singleton
fun provideGetDynamicReceiveAddressUseCase(
dynamicAddressesRepository: DynamicAddressesRepository,
): GetDynamicReceiveAddressUseCase {
return GetDynamicReceiveAddressUseCase(dynamicAddressesRepository)
}
@Provides
@Singleton
fun provideCreateConsolidationTransactionUseCase(
consolidationRepository: ConsolidationRepository,
): CreateConsolidationTransactionUseCase {
return CreateConsolidationTransactionUseCase(consolidationRepository)
}
@Provides
@Singleton
fun provideIsXpubSupportedUseCase(walletManagersFacade: WalletManagersFacade): IsXpubSupportedUseCase {
return IsXpubSupportedUseCase(walletManagersFacade)
}
@Provides
@Singleton
fun provideGetDerivedXpubUseCase(
walletManagersFacade: WalletManagersFacade,
derivationsRepository: DerivationsRepository,
): GetDerivedXpubUseCase {
return GetDerivedXpubUseCase(walletManagersFacade, derivationsRepository)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -226,8 +227,14 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
fun provideStakingIdFactory(
walletManagersFacade: WalletManagersFacade,
stakingFeatureToggles: StakingFeatureToggles,
): StakingIdFactory {
return StakingIdFactory(
walletManagersFacade = walletManagersFacade,
stakingFeatureToggles = stakingFeatureToggles,
)
}
@Provides

View file

@ -1,50 +0,0 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.tokensync.repository.TokenSyncRepository
import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TokenSyncDomainModule {
@Provides
@Singleton
fun provideObserveTokenSyncUseCase(tokenSyncRepository: TokenSyncRepository): ObserveTokenSyncUseCase {
return ObserveTokenSyncUseCase(
tokenSyncRepository = tokenSyncRepository,
)
}
@Provides
@Singleton
fun provideAcknowledgeTokenSyncCompletionUseCase(
tokenSyncRepository: TokenSyncRepository,
): AcknowledgeTokenSyncCompletionUseCase {
return AcknowledgeTokenSyncCompletionUseCase(
tokenSyncRepository = tokenSyncRepository,
)
}
@Provides
@Singleton
fun provideStartTokenSyncUseCase(
tokenSyncRepository: TokenSyncRepository,
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
appCoroutineScope: AppCoroutineScope,
): StartTokenSyncUseCase {
return StartTokenSyncUseCase(
tokenSyncRepository = tokenSyncRepository,
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
appCoroutineScope = appCoroutineScope,
)
}
}

View file

@ -5,6 +5,9 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.notifications.repository.PushNotificationsRepository
@ -257,10 +260,16 @@ internal object TransactionDomainModule {
fun provideReceiveAddressesFactory(
getEnsNameUseCase: GetEnsNameUseCase,
getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase,
dynamicAddressesRepository: DynamicAddressesRepository,
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
): ReceiveAddressesFactory {
return ReceiveAddressesFactory(
getEnsNameUseCase = getEnsNameUseCase,
getViewedTokenReceiveWarningUseCase = getViewedTokenReceiveWarningUseCase,
getDynamicReceiveAddressUseCase = getDynamicReceiveAddressUseCase,
dynamicAddressesRepository = dynamicAddressesRepository,
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.common.KeyPair
import com.tangem.common.SuccessResponse
import com.tangem.common.authentication.keystore.DummyKeystoreManager
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.InMemoryStorage
@ -32,6 +33,8 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.domain.sdk.mocks.showMockCardPicker
import com.tangem.tap.foregroundActivityObserver
@Suppress("TooManyFunctions")
class MockTangemSdkManager(
@ -61,6 +64,17 @@ class MockTangemSdkManager(
allowsRequestAccessCodeFromRepository: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse> {
if (!MockProvider.isPreset) {
val activity = foregroundActivityObserver.foregroundActivity
if (activity != null) {
val selectedMock = showMockCardPicker(activity)
if (selectedMock != null) {
MockProvider.setMocksWithoutPresetFlag(selectedMock)
} else {
return CompletionResult.Failure(TangemSdkError.UserCancelled())
}
}
}
return MockProvider.getScanResponse()
}

View file

@ -0,0 +1,33 @@
package com.tangem.tap.domain.sdk.mocks
import androidx.appcompat.app.AlertDialog
import com.tangem.wallet.R
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
internal suspend fun showMockCardPicker(activity: AppCompatActivity): MockContent? = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
val mocks = MockProvider.availableMocks
val names = mocks.map { it.first }.toTypedArray()
val dialog = AlertDialog.Builder(activity)
.setTitle(R.string.mock_card_picker_title)
.setItems(names) { _, which ->
if (continuation.isActive) {
continuation.resume(mocks[which].second)
}
}
.setOnCancelListener {
if (continuation.isActive) {
continuation.resume(null)
}
}
.create()
continuation.invokeOnCancellation { activity.runOnUiThread { dialog.dismiss() } }
dialog.show()
}
}

View file

@ -11,10 +11,32 @@ object MockProvider {
private var content: MockContent = getMockContent(ProductType.Wallet)
var isPreset: Boolean = false
private set
private var isEmulatingError: Boolean = false
private var emulatedError: TangemError = TangemSdkError.TagLost()
val availableMocks: List<Pair<String, MockContent>> = listOf(
"Wallet" to WalletMockContent,
"Note" to NoteMockContent,
"Twins" to TwinsMockContent,
"Ring" to RingMockContent,
"Wallet 2" to Wallet2MockContent,
"Wallet 2 (No Backup)" to Wallet2NoBackupMockContent,
"Wallet 2 (No Backup, No Wallets)" to Wallet2NoBackupNoWalletsMockContent,
"Wallet 2 (Seed Phrase)" to Wallet2WithSeedPhraseMockContent,
"Shiba" to ShibaMockContent,
"Shiba (No Backup)" to ShibaNoBackupMockContent,
"Shiba (No Backup, No Wallets)" to ShibaNoBackupNoWalletsMockContent,
"Ed25519 Curve" to EdCurveMockContent,
"Secp256k1 Curve" to Secpk1CurveMockContent,
"Backup Wallet" to BackupWalletMockContent,
"Dev Wallet" to DevWalletMockContent,
"Firmware 4.12" to Firmware412MockContent,
)
fun setEmulateError(error: TangemError? = null) {
isEmulatingError = true
error?.let {
@ -28,10 +50,16 @@ object MockProvider {
fun setMocks(productType: ProductType) {
content = getMockContent(productType)
isPreset = true
}
fun setMocks(mockContent: MockContent) {
content = mockContent
isPreset = true
}
fun setMocksWithoutPresetFlag(mockContent: MockContent) {
content = mockContent
}
fun getSuccessResponse() = CompletionResult.Success(content.successResponse).orFailure()

View file

@ -33,7 +33,7 @@ class FinalizeTwinTask(
visaCardScanHandler = null,
visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false,
isDynamicAddressesEnabled = false,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
onboardingV2FeatureToggles = null,
).run(session, callback)
is CompletionResult.Failure ->

View file

@ -1,55 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@Suppress("BooleanPropertyNaming")
sealed class DetailsAction : Action {
data class PrepareScreen(
val scanResponse: ScanResponse?,
val initializedAppSettingsState: AppSettingsState,
) : DetailsAction()
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
val setting: AppSetting,
) : AppSettings() {
data object Success : AppSettings()
data class Failure(
val prevState: Boolean,
val setting: AppSetting,
) : AppSettings()
}
data class CheckBiometricsStatus(
val coroutineScope: CoroutineScope,
) : AppSettings()
data object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val isEnrollBiometricsNeeded: Boolean,
) : AppSettings()
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
data class ChangeBalanceHiding(
val shouldHideBalance: Boolean,
) : AppSettings()
data class ChangeAppCurrency(
val currency: AppCurrency,
) : AppSettings()
data class Prepare(val state: AppSettingsState) : AppSettings()
}
data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction()
}

View file

@ -1,235 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
class DetailsMiddleware {
private val appSettingsMiddleware = AppSettingsMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
if (!DemoHelper.tryHandle(stateProvider)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(action)
}
}
next(action)
}
}
}
private fun handleAction(action: Action) {
when (action) {
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action)
}
}
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
observeBiometricsStatusChanges(action.coroutineScope)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
}
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
changeBalanceHiding(action.shouldHideBalance)
}
is DetailsAction.AppSettings.ChangeAppCurrency -> {
store.dispatch(GlobalAction.ChangeAppCurrency(action.currency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.currency))
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
private fun toggleBiometricsAuthentication(enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
walletsRepository.setRequireAccessCode(value = true)
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all saved access codes
removeAllBiometricSingData()
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach { wallet ->
userWalletsListRepository.setLock(
userWalletId = wallet.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach { wallet ->
if (wallet is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = wallet.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (isEnrollBiometricsNeeded != null) {
emit(isEnrollBiometricsNeeded)
}
delay(timeMillis = 200)
} while (true)
}
needEnrollBiometricsFlow
.distinctUntilChanged()
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.launchIn(scope)
.saveIn(checkBiometricsStatusJobHolder)
}
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
val repository = store.inject(DaggerGraphState::appThemeModeRepository)
scope.launch {
repository.changeAppThemeMode(appThemeMode)
}
}
private fun changeBalanceHiding(hideBalance: Boolean) {
val repository = store.inject(DaggerGraphState::balanceHidingRepository)
scope.launch {
val newState = repository.getBalanceHidingSettings().copy(
isHidingEnabledInSettings = hideBalance,
isBalanceHidden = false,
)
repository.storeBalanceHidingSettings(newState)
}
}
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
return tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = false)
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = false,
)
}
.doOnFailure { error ->
TangemLogger.e("Unable to delete saved access codes", error)
}
}
}
}

View file

@ -1,103 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
}
@Suppress("CyclomaticComplexMethod")
private fun internalReduce(action: Action, state: AppState): DetailsState {
if (action !is DetailsAction) return state.detailsState
val detailsState = state.detailsState
return when (action) {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
is DetailsAction.ChangeAppCurrency -> detailsState.copy(
appSettingsState = detailsState.appSettingsState.copy(
selectedAppCurrency = action.currency,
),
)
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
return DetailsState(
scanResponse = action.scanResponse,
appSettingsState = action.initializedAppSettingsState,
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
},
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
appSettingsState = state.appSettingsState.copy(
isInProgress = false,
),
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
appSettingsState = state.appSettingsState.copy(
needEnrollBiometrics = action.isEnrollBiometricsNeeded,
),
)
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedThemeMode = action.appThemeMode,
),
)
is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(
appSettingsState = state.appSettingsState.copy(
isHidingEnabled = action.shouldHideBalance,
),
)
// state should be copied to avoid concurrent modifications from different sources
is DetailsAction.AppSettings.Prepare -> state.copy(
appSettingsState = state.appSettingsState.copy(
isHidingEnabled = action.state.isHidingEnabled,
selectedAppCurrency = action.state.selectedAppCurrency,
selectedThemeMode = action.state.selectedThemeMode,
useBiometricAuthentication = action.state.useBiometricAuthentication,
requireAccessCode = action.state.requireAccessCode,
hasSecuredWallets = action.state.hasSecuredWallets,
),
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.StateType
data class DetailsState(
@Deprecated("Delete after onboarding refactoring")
val scanResponse: ScanResponse? = null,
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
@Suppress("BooleanPropertyNaming")
data class AppSettingsState(
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val hasSecuredWallets: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
val selectedAppCurrency: AppCurrency = AppCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
RequireAccessCode, BiometricAuthentication,
}

View file

@ -0,0 +1,3 @@
package com.tangem.tap.features.details.redux
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -2,71 +2,59 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
internal class AppSettingsDialogsFactory {
fun createThemeModeSelectorDialog(
selectedModeIndex: Int,
onSelect: (AppThemeMode) -> Unit,
onDismiss: () -> Unit,
): Dialog.Selector {
val modes = AppThemeMode.available
return Dialog.Selector(
title = resourceReference(R.string.app_settings_theme_selector_title),
selectedItemIndex = selectedModeIndex,
items = modes.map { mode ->
resourceReference(
id = when (mode) {
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
},
)
}.toImmutableList(),
onSelect = { index ->
val mode = AppThemeMode.available[index]
onSelect(mode)
},
onDismiss = onDismiss,
)
}
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(
message = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_disable),
isWarning = true,
onClick = onDisable,
)
},
secondActionBuilder = { cancelAction() },
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
message = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_enable),
onClick = onEnable,
)
},
secondActionBuilder = { cancelAction() },
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
message = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_disable),
isWarning = true,
onClick = onDisable,
)
},
secondActionBuilder = { cancelAction() },
)
}
}

View file

@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -42,13 +40,6 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
@Composable
private fun AppSettings(state: AppSettingsScreenState.Content) {
val dialog by rememberUpdatedState(newValue = state.dialog)
when (val safeDialog = dialog) {
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
null -> Unit
}
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
@ -102,12 +93,7 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
)
add(
AppSettingsScreenState.Content(
items = items,
dialog = null,
),
)
add(AppSettingsScreenState.Content(items = items))
},
)
// endregion Preview

View file

@ -10,10 +10,7 @@ internal sealed class AppSettingsScreenState {
object Loading : AppSettingsScreenState()
data class Content(
val items: ImmutableList<Item>,
val dialog: Dialog?,
) : AppSettingsScreenState()
data class Content(val items: ImmutableList<Item>) : AppSettingsScreenState()
@Immutable
sealed class Item {
@ -46,26 +43,4 @@ internal sealed class AppSettingsScreenState {
val onClick: () -> Unit,
) : Item()
}
@Immutable
sealed class Dialog {
abstract val onDismiss: () -> Unit
data class Alert(
val title: TextReference,
val description: TextReference,
val confirmText: TextReference,
val onConfirm: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
data class Selector(
val title: TextReference,
val selectedItemIndex: Int,
val items: ImmutableList<TextReference>,
val onSelect: (Int) -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
}
}

View file

@ -4,42 +4,56 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.essenty.lifecycle.doOnResume
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppSettingsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Suppress("UnusedPrivateMember") @Assisted params: Unit,
) : AppSettingsComponent, AppComponentContext by appComponentContext {
private val model: AppSettingsModel = getOrCreateModel()
init {
private val dialogSlot = childSlot(
source = model.dialogNavigation,
serializer = AppSettingsDialogConfig.serializer(),
handleBackButton = true,
childFactory = { config, _ -> config },
)
init {
doOnResume { model.onResume() }
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val dialog by dialogSlot.subscribeAsState()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
onBackClick = model::onBackClick,
)
dialog.child?.instance?.let { config ->
when (config) {
is AppSettingsDialogConfig.ThemeModeSelector -> SettingsSelectorDialog(
config = config,
onSelect = model::onThemeModeSelected,
onDismiss = model::dismissDialog,
)
}
}
}
@AssistedFactory

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun SettingsSelectorDialog(
config: AppSettingsDialogConfig.ThemeModeSelector,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
) {
val modes = AppThemeMode.available
SelectorDialog(
title = stringResourceSafe(R.string.app_settings_theme_selector_title),
selectedItemIndex = config.selectedModeIndex,
items = modes.map { mode ->
stringResourceSafe(
id = when (mode) {
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
},
)
}.toImmutableList(),
confirmButton = DialogButtonUM(
title = stringResourceSafe(R.string.common_cancel),
onClick = onDismiss,
),
onSelect = onSelect,
onDismissDialog = onDismiss,
)
}

View file

@ -1,54 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@Composable
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
BasicDialog(
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
isWarning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemThemePreview {
SettingsAlertDialog(dialog = dialog)
}
}
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog>(
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {}))
add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {}))
},
)
// endregion Preview

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
SelectorDialog(
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButtonUM(
title = stringResourceSafe(R.string.common_cancel),
onClick = dialog.onDismiss,
),
onSelect = dialog.onSelect,
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SettingsSelectorDialogPreview(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemThemePreview {
SettingsSelectorDialog(param)
}
}
private class DialogProvider : CollectionPreviewParameterProvider<Dialog.Selector>(
collection = listOf(
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
selectedModeIndex = 0,
onSelect = {},
onDismiss = {},
),
),
)
// endregion Preview

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings.model
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface AppSettingsDialogConfig {
@Serializable
data class ThemeModeSelector(val selectedModeIndex: Int) : AppSettingsDialogConfig
}

View file

@ -1,11 +1,18 @@
package com.tangem.tap.features.details.ui.appsettings.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.appcurrency.model.AppCurrency
@ -13,113 +20,138 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import com.tangem.tap.features.details.ui.appsettings.analytics.AppSettingsItemsAnalyticsSender
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.extensions.addIf
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
internal class AppSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appCurrencyRepository: AppCurrencyRepository,
appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val balanceHidingRepository: BalanceHidingRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appThemeModeRepository: AppThemeModeRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val tangemSdkManager: TangemSdkManager,
private val settingsManager: SettingsManager,
private val settingsRepository: SettingsRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val tangemHotSdk: TangemHotSdk,
private val router: Router,
private val uiMessageSender: UiMessageSender,
) : Model(), StoreSubscriber<DetailsState> {
) : Model() {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
private val appCurrencyUpdatesJobHolder = JobHolder()
val dialogNavigation: SlotNavigation<AppSettingsDialogConfig> = SlotNavigation()
private val _uiState: MutableStateFlow<AppSettingsScreenState> = MutableStateFlow(
value = AppSettingsScreenState.Loading,
)
val uiState: StateFlow<AppSettingsScreenState> = _uiState
private val localState = MutableStateFlow(LocalState())
private val biometricsStatusJobHolder = JobHolder()
val uiState: StateFlow<AppSettingsScreenState>
field = MutableStateFlow<AppSettingsScreenState>(value = AppSettingsScreenState.Loading)
init {
bootstrapAppCurrencyUpdates()
bootstrapBiometricsUpdates()
bootstrapLocalState()
combine(
flow = appCurrencyRepository.getSelectedAppCurrency().distinctUntilChanged(),
flow2 = appThemeModeRepository.getAppThemeMode(),
flow3 = balanceHidingRepository.getBalanceHidingSettingsFlow(),
flow4 = localState,
) { currency, themeMode, hidingSettings, local ->
AppSettingsState(
appCurrency = currency,
themeMode = themeMode,
isHidingEnabled = hidingSettings.isHidingEnabledInSettings,
local = local,
)
}
.onEach { state ->
val items = buildItems(state)
uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> prevState.copy(items = items)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(items = items)
}
}
}
.launchIn(modelScope)
subscribeToStoreChanges()
sendItemsAnalytics()
}
override fun newState(state: DetailsState) {
val items = buildItems(state.appSettingsState)
_uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> prevState.copy(
items = items,
)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(
items = items,
dialog = null,
)
}
}
}
fun onResume() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(modelScope))
observeBiometricsStatusChanges()
}
override fun onDestroy() {
super.onDestroy()
store.unsubscribe(subscriber = this)
private fun observeBiometricsStatusChanges() {
flow {
do {
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (isEnrollBiometricsNeeded != null) {
emit(isEnrollBiometricsNeeded)
}
delay(timeMillis = 200)
} while (true)
}
.flowOn(dispatchers.default)
.distinctUntilChanged()
.onEach { isEnrollBiometricsNeeded ->
localState.update { it.copy(isEnrollBiometricsNeeded = isEnrollBiometricsNeeded) }
}
.launchIn(modelScope)
.saveIn(biometricsStatusJobHolder)
}
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
val items = buildList {
addIf(
condition = state.needEnrollBiometrics,
condition = state.local.isEnrollBiometricsNeeded,
element = itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics),
)
add(
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = state.selectedAppCurrency.name,
currentAppCurrencyName = state.appCurrency.name,
onClick = ::showAppCurrencySelector,
),
)
val canUseBiometrics =
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
val canUseBiometrics = with(state.local) {
!isEnrollBiometricsNeeded && !isInProgress && hasSecuredWallets
}
add(
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isChecked = state.local.isBiometricAuthenticationUsed,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
@ -128,8 +160,8 @@ internal class AppSettingsModel @Inject constructor(
add(
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
isChecked = state.local.isAccessCodeRequired || !state.local.isBiometricAuthenticationUsed,
isEnabled = canUseBiometrics && state.local.isBiometricAuthenticationUsed,
onCheckedChange = ::onRequireAccessCodeToggled,
),
)
@ -144,8 +176,8 @@ internal class AppSettingsModel @Inject constructor(
add(
itemsFactory.createSelectThemeModeButton(
currentThemeMode = state.selectedThemeMode,
onClick = { showThemeModeSelector(state.selectedThemeMode) },
currentThemeMode = state.themeMode,
onClick = { showThemeModeSelector(state.themeMode) },
),
)
}
@ -154,31 +186,35 @@ internal class AppSettingsModel @Inject constructor(
}
private fun enrollBiometrics() {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
analyticsEventHandler.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
settingsManager.openBiometricSettings()
}
fun onBackClick() {
router.pop()
}
private fun showAppCurrencySelector() {
store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) }
router.push(AppRoute.AppCurrencySelector)
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
updateContentState {
copy(
dialog = dialogsFactory.createThemeModeSelectorDialog(
selectedModeIndex = selectedMode.ordinal,
onSelect = { mode ->
analyticsEventHandler.send(
event = Settings.AppSettings.ThemeSwitched(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
)
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
dialogNavigation.activate(AppSettingsDialogConfig.ThemeModeSelector(selectedMode.ordinal))
}
fun onThemeModeSelected(index: Int) {
val mode = AppThemeMode.available[index]
analyticsEventHandler.send(
event = Settings.AppSettings.ThemeSwitched(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
)
changeAppThemeMode(mode)
dialogNavigation.dismiss()
}
fun dismissDialog() {
dialogNavigation.dismiss()
}
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
@ -186,19 +222,13 @@ internal class AppSettingsModel @Inject constructor(
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
toggleBiometricsAuthentication(enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
uiMessageSender.send(
dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = { toggleBiometricsAuthentication(enable = false) },
),
)
}
}
@ -210,74 +240,136 @@ internal class AppSettingsModel @Inject constructor(
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
if (isChecked) {
uiMessageSender.send(
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = { toggleRequireAccessCode(enable = true) },
),
)
} else {
uiMessageSender.send(
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = { toggleRequireAccessCode(enable = false) },
),
)
}
}
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
private fun toggleBiometricsAuthentication(enable: Boolean) {
localState.update { it.copy(isBiometricAuthenticationUsed = enable, isInProgress = true) }
modelScope.launch {
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
localState.update { it.copy(isInProgress = false) }
return@launch
}
if (enable) {
setBiometricLockForAllWallets()
} else {
removeAllBiometricData()
walletsRepository.setRequireAccessCode(value = true)
localState.update { it.copy(isAccessCodeRequired = true) }
}
walletsRepository.setUseBiometricAuthentication(value = enable)
localState.update { it.copy(isInProgress = false) }
}
}
private fun toggleRequireAccessCode(enable: Boolean) {
localState.update { it.copy(isAccessCodeRequired = enable, isInProgress = true) }
modelScope.launch {
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
localState.update { it.copy(isInProgress = false) }
return@launch
}
if (enable) {
removeAllBiometricSingData(userWalletsListRepository.userWalletsSync())
}
walletsRepository.setRequireAccessCode(value = enable)
localState.update { it.copy(isInProgress = false) }
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach { wallet ->
userWalletsListRepository.setLock(
userWalletId = wallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData(userWallets)
}
private suspend fun removeAllBiometricSingData(userWallets: List<UserWallet>) {
deleteSavedAccessCodes()
userWallets.forEach { wallet ->
if (wallet is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = wallet.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId),
),
)
}
}
}
private suspend fun deleteSavedAccessCodes() {
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
analyticsEventHandler.send(
Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off),
)
settingsRepository.setShouldSaveAccessCodes(value = false)
cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false)
}
.doOnFailure { error ->
TangemLogger.e("Unable to delete saved access codes", error)
}
}
private fun onFlipToHideBalanceToggled(enable: Boolean) {
val param = AnalyticsParam.OnOffState(enable)
analyticsEventHandler.send(Settings.AppSettings.HideBalanceChanged(param))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(shouldHideBalance = enable))
modelScope.launch {
val settings = balanceHidingRepository.getBalanceHidingSettings().copy(
isHidingEnabledInSettings = enable,
isBalanceHidden = false,
)
balanceHidingRepository.storeBalanceHidingSettings(settings)
}
}
private fun dismissDialog() {
updateContentState { copy(dialog = null) }
private fun changeAppThemeMode(mode: AppThemeMode) {
modelScope.launch {
appThemeModeRepository.changeAppThemeMode(mode)
}
}
private fun bootstrapAppCurrencyUpdates() {
appCurrencyRepository
.getSelectedAppCurrency()
.onEach { appCurrency ->
if (appCurrency.code == store.state.globalState.appCurrency.code) return@onEach
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency))
}
.launchIn(scope)
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = modelScope.launch {
val state = AppSettingsState(
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
)
store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state))
}
private fun subscribeToStoreChanges() {
store.subscribe(subscriber = this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
private fun bootstrapLocalState() = modelScope.launch {
localState.update { state ->
state.copy(
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
isBiometricAuthenticationUsed = walletsRepository.useBiometricAuthentication(),
isAccessCodeRequired = walletsRepository.requireAccessCode(),
)
}
}
@ -286,15 +378,21 @@ internal class AppSettingsModel @Inject constructor(
.filterIsInstance<AppSettingsScreenState.Content>()
.distinctUntilChangedBy(AppSettingsScreenState.Content::items)
.onEach { appSettingsItemsAnalyticsSender.send(it.items) }
.launchIn(scope)
.launchIn(modelScope)
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
_uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> block(prevState)
is AppSettingsScreenState.Loading -> prevState
}
}
}
private data class LocalState(
val hasSecuredWallets: Boolean = false,
val isEnrollBiometricsNeeded: Boolean = false,
val isBiometricAuthenticationUsed: Boolean = false,
val isAccessCodeRequired: Boolean = false,
val isInProgress: Boolean = false,
)
private data class AppSettingsState(
val themeMode: AppThemeMode = AppThemeMode.DEFAULT,
val isHidingEnabled: Boolean = false,
val appCurrency: AppCurrency = AppCurrency.Default,
val local: LocalState = LocalState(),
)
}

View file

@ -188,7 +188,7 @@ internal class DefaultRampManager(
private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean {
val currencyAssedId = ExpressAsset.ID(
networkId = this.network.backendId,
networkId = this.network.rawId,
contractAddress = (this as? CryptoCurrency.Token)?.contractAddress,
)

View file

@ -138,6 +138,7 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
AppRoute.ManageTokens.Source.WALLET -> ManageTokensSource.WALLET
}
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None

View file

@ -2,5 +2,6 @@
<resources>
<string name="tangem_app_name" translatable="false">Tangem</string>
<string name="mock_card_picker_title" translatable="false">Select Mock Card</string>
</resources>

View file

@ -146,6 +146,7 @@ sealed class AppRoute(val path: String) : Route {
STORIES,
SETTINGS,
ACCOUNT,
WALLET,
}
}

View file

@ -47,7 +47,7 @@ fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal
* StakeKit-specific extension to get total balance including rewards.
*/
private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeKit(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
return if (BlockchainUtils.isIncludeStakingTotalBalance(networkId = blockchainId)) {
balance.items.sumOf { it.amount }
} else {
getRewardStakingBalance()
@ -58,7 +58,7 @@ private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeK
* StakeKit-specific extension to get total staked balance excluding rewards.
*/
private fun StakingBalance.Data.StakeKit.getTotalStakingBalanceStakeKit(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
return if (BlockchainUtils.isIncludeStakingTotalBalance(networkId = blockchainId)) {
balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount }

View file

@ -55,8 +55,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
)
val network = Network(
id = Network.ID(blockchain.id, derivationPath),
backendId = blockchain.toNetworkId(),
id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
@ -85,12 +84,11 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(blockchain.id),
body = CryptoCurrency.ID.Body.NetworkId(blockchain.toNetworkId()),
suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id),
),
network = Network(
id = Network.ID(value = blockchain.id, derivationPath),
backendId = "NEVER-MIND",
id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath),
name = blockchain.fullName,
currencySymbol = "NEVER-MIND",
derivationPath = derivationPath,

View file

@ -1,6 +1,8 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -12,13 +14,23 @@ dependencies {
/** Project - Core */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
/** Project - Common */
implementation(projects.common.uiCharts)
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.offramp)
implementation(projects.domain.demo)
implementation(deps.lifecycle.compose)
implementation(deps.compose.foundation)
@ -26,4 +38,8 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.kotlin.immutable.collections)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,11 @@
package com.tangem.common.ui.markets.action
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.model.TokenActionsState
data class CryptoCurrencyData(
val userWallet: UserWallet,
val status: CryptoCurrencyStatus,
val actions: List<TokenActionsState.ActionState>,
)

View file

@ -1,14 +1,14 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
package com.tangem.common.ui.markets.action
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.feed.impl.R
@Immutable
internal sealed class QuickActionUM(
sealed class QuickActionUM(
val title: TextReference,
val description: TextReference,
@DrawableRes val icon: Int,

View file

@ -0,0 +1,9 @@
package com.tangem.common.ui.markets.action
import kotlinx.collections.immutable.ImmutableList
data class QuickActions(
val actions: ImmutableList<QuickActionUM>,
val onQuickActionClick: (QuickActionUM) -> Unit,
val onQuickActionLongClick: (QuickActionUM) -> Unit,
)

View file

@ -0,0 +1,61 @@
package com.tangem.common.ui.markets.action
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import kotlinx.collections.immutable.toImmutableList
object QuickActionsConverter {
fun quickActions(cryptoData: CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): QuickActions {
return QuickActions(
actions = toQuickActions(cryptoData.actions),
onQuickActionClick = { quickActionUM ->
when (quickActionUM) {
QuickActionUM.Buy -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Buy,
cryptoCurrencyData = cryptoData,
)
is QuickActionUM.Exchange -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Exchange,
cryptoCurrencyData = cryptoData,
)
QuickActionUM.Receive -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Receive,
cryptoCurrencyData = cryptoData,
)
QuickActionUM.Stake -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Stake,
cryptoCurrencyData = cryptoData,
)
is QuickActionUM.YieldMode -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.YieldMode,
cryptoCurrencyData = cryptoData,
)
}
},
onQuickActionLongClick = { actionUM ->
if (actionUM == QuickActionUM.Receive) {
tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.CopyAddress,
cryptoCurrencyData = cryptoData,
)
}
},
)
}
fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
actions.forEach { action ->
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
when (action) {
is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(action.shouldShowBadge)
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(action.apy)
else -> null
}?.let(::add)
}
}
}.toImmutableList()
}

View file

@ -1,14 +1,14 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
package com.tangem.common.ui.markets.action
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.feed.impl.R
import kotlinx.collections.immutable.ImmutableList
internal data class TokenActionsBSContentUM(
data class TokenActionsBSContentUM(
val title: String,
val actions: ImmutableList<Action>,
val onActionClick: (Action) -> Unit,

View file

@ -1,7 +1,8 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
package com.tangem.common.ui.markets.action
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.common.ui.markets.R
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
@ -18,9 +19,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.offramp.GetOfframpUrlUseCase
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.feed.impl.R
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -28,7 +26,7 @@ import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.toImmutableList
@Suppress("LongParameterList")
internal class TokenActionsHandler @AssistedInject constructor(
class TokenActionsHandler @AssistedInject constructor(
private val router: Router,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
@ -45,7 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
add(TokenActionsBSContentUM.Action.Sell)
}
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) {
onHandleQuickAction(
HandledQuickAction(
action = action,
@ -79,13 +77,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
private fun showDemoModeWarning() {
val message = DialogMessage(
val message = DialogMessage.Companion(
message = resourceReference(R.string.alert_demo_feature_disabled),
)
messageSender.send(message)
}
private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onCopyAddress(cryptoCurrencyData: CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
val addresses = networkAddress.availableAddresses
@ -97,7 +95,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)))
}
private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onBuyClick(cryptoCurrencyData: CryptoCurrencyData) {
router.push(
AppRoute.Onramp(
userWalletId = cryptoCurrencyData.userWallet.walletId,
@ -107,7 +105,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
)
}
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onSellClick(cryptoCurrencyData: CryptoCurrencyData) {
getOfframpUrlUseCase(
cryptoCurrencyStatus = cryptoCurrencyData.status,
appCurrencyCode = currentAppCurrency().code,
@ -117,7 +115,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
}
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onExchangeClick(cryptoCurrencyData: CryptoCurrencyData) {
router.push(
AppRoute.Swap(
currencyFrom = cryptoCurrencyData.status.currency,
@ -128,7 +126,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
)
}
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onSendClick(cryptoCurrencyData: CryptoCurrencyData) {
val route = AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
@ -136,7 +134,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
router.push(route)
}
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onStakeClick(cryptoCurrencyData: CryptoCurrencyData) {
val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake }
?.let { it as TokenActionsState.ActionState.Stake }
?.option ?: return
@ -150,7 +148,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
)
}
private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
private fun onYieldModeClick(cryptoCurrencyData: CryptoCurrencyData) {
val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>()
.firstOrNull()?.apy ?: return
@ -173,6 +171,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
data class HandledQuickAction(
val action: TokenActionsBSContentUM.Action,
val cryptoCurrencyData: PortfolioData.CryptoCurrencyData,
val cryptoCurrencyData: CryptoCurrencyData,
)
}

View file

@ -27,6 +27,8 @@ class AccountCryptoPortfolioItemStateConverter(
private val priceChangeLce: Lce<Unit, PriceChange>? = null,
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?)? = null,
private val subtitle2StateProvider: ((Lce<Unit, PriceChange>) -> Subtitle2State?)? = null,
) : Converter<TotalFiatBalance, TokenItemState> {
override fun convert(value: TotalFiatBalance): TokenItemState {
@ -40,11 +42,11 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
val subtitle2State = priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
val subtitle2State = if (priceChangeLce != null && subtitle2StateProvider != null) {
subtitle2StateProvider(priceChangeLce)
} else {
createSubtitle2State(priceChangeLce)
}
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter().convert(this),
@ -59,11 +61,8 @@ class AccountCryptoPortfolioItemStateConverter(
),
isAvailable = false,
),
fiatAmountState = FiatAmountState.Content(
text = fiatBalance.amount
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
isFlickering = fiatBalance.source == StatusSource.CACHE,
),
fiatAmountState = fiatAmountStateProvider?.invoke(fiatBalance)
?: createFiatAmountState(fiatBalance, appCurrency),
subtitle2State = subtitle2State,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
@ -127,4 +126,30 @@ class AccountCryptoPortfolioItemStateConverter(
type = this.value.getPriceChangeType(),
isFlickering = this.source.isFlickering(),
)
private fun createFiatAmountState(fiatBalance: TotalFiatBalance, appCurrency: AppCurrency): FiatAmountState {
return when (fiatBalance) {
TotalFiatBalance.Failed,
TotalFiatBalance.Loading,
-> FiatAmountState.Empty
is TotalFiatBalance.Loaded -> FiatAmountState.Content(
text = fiatBalance.amount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
isFlickering = fiatBalance.source == StatusSource.CACHE,
)
}
}
private fun createSubtitle2State(priceChangeLce: Lce<Unit, PriceChange>?): Subtitle2State? {
return priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.common.ui.navigationButtons
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()

View file

@ -15,24 +15,8 @@
"name": "vanar-chain",
"version": "undefined"
},
{
"name": "sonic",
"version": "5.21.0"
},
{
"name": "apechain",
"version": "5.21.0"
},
{
"name": "alephium",
"version": "5.21.0"
},
{
"name": "zklink",
"version": "undefined"
},
{
"name": "plasma",
"version": "5.31"
}
]

View file

@ -73,7 +73,7 @@
"version": "undefined"
},
{
"name": "TOKEN_SYNC_ENABLED",
"name": "ASSETS_DISCOVERY_ENABLED",
"version": "undefined"
},
{
@ -83,5 +83,9 @@
{
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
"version": "undefined"
},
{
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
"version": "undefined"
}
]

View file

@ -96,4 +96,10 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
@Body body: WithdrawRequest,
): ApiResponse<WithdrawResponse>
@PATCH("v1/card")
suspend fun updateCardDisplayName(
@Header("Authorization") authHeader: String,
@Body body: UpdateCardDisplayNameRequest,
): ApiResponse<UpdateCardDisplayNameResponse>
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UpdateCardDisplayNameRequest(
@Json(name = "display_name") val displayName: String,
)

View file

@ -29,6 +29,7 @@ data class CustomerMeResponse(
@Json(name = "status") val status: Status,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
@Json(name = "display_name") val displayName: String?,
) {
@JsonClass(generateAdapter = false)
enum class Status {

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UpdateCardDisplayNameResponse(
@Json(name = "result") val result: Result?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "display_name") val displayName: String?,
)
}

View file

@ -42,7 +42,8 @@ data class UserTokensResponse(
return otherToken.contractAddress == this.contractAddress &&
otherToken.networkId == this.networkId &&
otherToken.derivationPath == this.derivationPath &&
otherToken.decimals == this.decimals
otherToken.decimals == this.decimals &&
otherToken.dynamicAddressesEnabled == this.dynamicAddressesEnabled
}
override fun hashCode(): Int = calculateHashCode(
@ -50,6 +51,7 @@ data class UserTokensResponse(
networkId.hashCode(),
derivationPath.hashCode(),
decimals.hashCode(),
dynamicAddressesEnabled.hashCode(),
)
}

View file

@ -29,11 +29,12 @@ sealed interface NetworkStatusDM {
/**
* Verified
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
* @property yieldSupplyStatuses yield supply statuses
*/
@NameLabel("amounts")
data class Verified(
@ -65,6 +66,11 @@ sealed interface NetworkStatusDM {
@Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM
/**
* Id
*
* @property value blockchain id [com.tangem.blockchain.common.Blockchain.id]
*/
@JsonClass(generateAdapter = true)
data class ID(
@Json(name = "value") val value: String,

View file

@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PR
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
import com.tangem.datasource.local.preferences.utils.SwapCurrencyIdMigration
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
@ -80,6 +81,7 @@ internal object PreferencesDataStore {
legacyKeyName = LEGACY_DEFAULT_KEY_NAME,
keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name,
),
SwapCurrencyIdMigration(),
CleanupKeyMigration(key = APP_LOGS_KEY),
CleanupKeyMigration(key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY),
CleanupKeyMigration(key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY),

View file

@ -136,7 +136,7 @@ object PreferencesKeys {
val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") }
val PENDING_DISCOVERY_SYNC_KEY by lazy { stringPreferencesKey(name = "pendingDiscoverySync") }
val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

View file

@ -0,0 +1,151 @@
package com.tangem.datasource.local.preferences.utils
import androidx.datastore.core.DataMigration
import androidx.datastore.preferences.core.Preferences
import com.tangem.datasource.local.preferences.PreferencesKeys
/**
* Migrates cached CryptoCurrency.ID strings from old blockchain.id format to new networkId format.
*
* After refactoring, Network.rawId stores backendId values (e.g. "ethereum") instead of
* blockchain.id values (e.g. "ETH"). CryptoCurrency.ID body contains this value, so cached IDs
* like "coin⟨ETH⟩ethereum" must become "coin⟨ethereum⟩ethereum".
*
* Affected DataStore keys: [PreferencesKeys.SWAP_TRANSACTIONS_KEY],
* [PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY].
*/
internal class SwapCurrencyIdMigration : DataMigration<Preferences> {
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
return currentData.contains(PreferencesKeys.SWAP_TRANSACTIONS_KEY) ||
currentData.contains(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY)
}
override suspend fun migrate(currentData: Preferences): Preferences {
val mutablePrefs = currentData.toMutablePreferences()
currentData[PreferencesKeys.SWAP_TRANSACTIONS_KEY]?.let { json ->
mutablePrefs[PreferencesKeys.SWAP_TRANSACTIONS_KEY] = migrateJson(json)
}
currentData[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]?.let { json ->
mutablePrefs[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY] = migrateJson(json)
}
return mutablePrefs.toPreferences()
}
override suspend fun cleanUp() {
// nothing to clean up
}
/**
* Replaces old blockchain.id values with networkId values inside CryptoCurrency.ID strings
* found anywhere in the JSON. Works by finding all `oldId` and `oldId` patterns and
* replacing the old ID with the new one.
*/
private fun migrateJson(json: String): String {
var result = json
for ((oldId, newId) in BLOCKCHAIN_ID_TO_NETWORK_ID) {
// Body without derivation path: ⟨oldId⟩ → ⟨newId⟩
result = result.replace("$BODY_START$oldId$BODY_END", "$BODY_START$newId$BODY_END")
// Body with derivation path: ⟨oldId→ → ⟨newId→
result = result.replace(
"$BODY_START$oldId$DERIVATION_DELIMITER",
"$BODY_START$newId$DERIVATION_DELIMITER",
)
}
return result
}
private companion object {
const val BODY_START = '\u27E8' // ⟨
const val BODY_END = '\u27E9' // ⟩
const val DERIVATION_DELIMITER = '\u2192' // →
/** Mapping of old blockchain.id → new networkId (only entries where values differ). */
val BLOCKCHAIN_ID_TO_NETWORK_ID = mapOf(
"ARBITRUM-ONE" to "arbitrum-one",
"ARBITRUM/test" to "arbitrum-one/test",
"AVALANCHE" to "avalanche",
"AVALANCHE/test" to "avalanche/test",
"BINANCE" to "binancecoin",
"BINANCE/test" to "binancecoin/test",
"BSC" to "binance-smart-chain",
"BSC/test" to "binance-smart-chain/test",
"BTC" to "bitcoin",
"BTC/test" to "bitcoin/test",
"BCH" to "bitcoin-cash",
"BCH/test" to "bitcoin-cash/test",
"CARDANO-S" to "cardano",
"DOGE" to "dogecoin",
"DUC" to "ducatus",
"ETH" to "ethereum",
"ETH/test" to "ethereum/test",
"ETC" to "ethereum-classic",
"ETC/test" to "ethereum-classic/test",
"ETH-Pow" to "ethereum-pow-iou",
"ETH-Pow/test" to "ethereum-pow-iou/test",
"FTM" to "fantom",
"FTM/test" to "fantom/test",
"GNO" to "xdai",
"KAS" to "kaspa",
"KAS/test" to "kaspa/test",
"KAVA" to "kava",
"KAVA/test" to "kava/test",
"Kusama" to "kusama",
"LTC" to "litecoin",
"NEAR" to "near-protocol",
"NEAR/test" to "near-protocol/test",
"NEXA" to "nexa",
"NEXA/test" to "nexa/test",
"OPTIMISM" to "optimistic-ethereum",
"Polkadot" to "polkadot",
"POLYGON" to "polygon-pos",
"POLYGON/test" to "polygon-pos/test",
"RSK" to "rootstock",
"SOLANA" to "solana",
"SOLANA/test" to "solana/test",
"TELOS" to "telos",
"TELOS/test" to "telos/test",
"The-Open-Network" to "the-open-network",
"The-Open-Network/test" to "the-open-network/test",
"TRON" to "tron",
"TRON/test" to "tron/test",
"XLM" to "stellar",
"XLM/test" to "stellar/test",
"XRP" to "xrp",
"XTZ" to "tezos",
"DASH" to "dash",
"xdc" to "xdc-network",
"xdc/test" to "xdc-network/test",
"hedera" to "hedera-hashgraph",
"hedera/test" to "hedera-hashgraph/test",
"areon" to "areon-network",
"areon/test" to "areon-network/test",
"pls" to "pulsechain",
"pls/test" to "pulsechain/test",
"zkSyncEra" to "zksync",
"zkSyncEra/test" to "zksync/test",
"polygonZkEVM" to "polygon-zkevm",
"polygonZkEVM/test" to "polygon-zkevm/test",
"flare" to "flare-network",
"flare/test" to "flare-network/test",
"playa3ull" to "playa3ull-games",
"sei" to "sei-network",
"sei/test" to "sei-network/test",
"casper" to "casper-network",
"casper/test" to "casper-network/test",
"odyssey" to "dione",
"odyssey/test" to "dione/test",
"hyperliquid" to "hyperevm",
"hyperliquid/test" to "hyperevm/test",
"quai" to "quai-network",
"quai/test" to "quai-network/test",
"manta/test" to "manta-pacific/test",
"dischain" to "ethereumfair",
)
}
}

View file

@ -43,6 +43,7 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "is_pin_set") val isPinSet: Boolean,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "display_name") val displayName: String?,
) : PaymentAccountStatusValueDM
@NameLabel("card_issue_failed")

View file

@ -0,0 +1,224 @@
package com.tangem.datasource.local.preferences.utils
import androidx.datastore.preferences.core.mutablePreferencesOf
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.preferences.PreferencesKeys
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class SwapCurrencyIdMigrationTest {
private val migration = SwapCurrencyIdMigration()
// region shouldMigrate
@Test
fun `shouldMigrate returns true when swap transactions key exists`() = runTest {
val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]")
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns true when last swapped currency key exists`() = runTest {
val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]")
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns true when both keys exist`() = runTest {
val prefs = mutablePreferencesOf(
PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]",
PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]",
)
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns false when no keys exist`() = runTest {
val prefs = mutablePreferencesOf()
assertThat(migration.shouldMigrate(prefs)).isFalse()
}
// endregion
// region migrate — coin IDs without derivation path
@Test
fun `migrates simple coin ID - ETH to ethereum`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${BE}ethereum"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${BE}ethereum"))
}
@Test
fun `migrates simple coin ID - BTC to bitcoin`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}BTC${BE}bitcoin"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}bitcoin${BE}bitcoin"))
}
@Test
fun `migrates BSC to binance-smart-chain`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}BSC${BE}binancecoin"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}binance-smart-chain${BE}binancecoin"))
}
// endregion
// region migrate — coin IDs with derivation path
@Test
fun `migrates coin ID with derivation path`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${DP}12367123${BE}ethereum"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${DP}12367123${BE}ethereum"))
}
@Test
fun `migrates POLYGON with derivation path`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}POLYGON${DP}99999${BE}polygon-pos"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}polygon-pos${DP}99999${BE}polygon-pos"))
}
// endregion
// region migrate — token IDs
@Test
fun `migrates token ID with contract address`() = runTest {
val result = migrateLastSwapped(currencyIdJson("token${BS}ETH${BE}usdt${CA}0xdAC17"))
assertThat(result).isEqualTo(currencyIdJson("token${BS}ethereum${BE}usdt${CA}0xdAC17"))
}
@Test
fun `migrates token ID with derivation path and contract address`() = runTest {
val result = migrateLastSwapped(
currencyIdJson("token${BS}ETH${DP}12345${BE}usdt${CA}0xdAC17"),
)
assertThat(result).isEqualTo(
currencyIdJson("token${BS}ethereum${DP}12345${BE}usdt${CA}0xdAC17"),
)
}
// endregion
// region migrate — swap transactions (both from and to IDs)
@Test
fun `migrates both fromCryptoCurrencyId and toCryptoCurrencyId`() = runTest {
val from = "coin${BS}ETH${BE}ethereum"
val to = "coin${BS}BTC${BE}bitcoin"
val oldJson = "[{\"fromCryptoCurrencyId\":\"$from\",\"toCryptoCurrencyId\":\"$to\"}]"
val expectedFrom = "coin${BS}ethereum${BE}ethereum"
val expectedTo = "coin${BS}bitcoin${BE}bitcoin"
val expected = "[{\"fromCryptoCurrencyId\":\"$expectedFrom\",\"toCryptoCurrencyId\":\"$expectedTo\"}]"
val result = migrateSwapTransactions(oldJson)
assertThat(result).isEqualTo(expected)
}
// endregion
// region migrate — no-op cases
@Test
fun `does not modify already migrated IDs`() = runTest {
val json = currencyIdJson("coin${BS}ethereum${BE}ethereum")
val result = migrateLastSwapped(json)
assertThat(result).isEqualTo(json)
}
@Test
fun `does not modify IDs where blockchain id equals networkId`() = runTest {
val json = currencyIdJson("coin${BS}cosmos${BE}cosmos")
val result = migrateLastSwapped(json)
assertThat(result).isEqualTo(json)
}
@Test
fun `does not modify empty list`() = runTest {
val result = migrateLastSwapped("[]")
assertThat(result).isEqualTo("[]")
}
// endregion
// region migrate — multiple entries
@Test
fun `migrates multiple entries in list`() = runTest {
val old1 = currencyIdValue("coin${BS}ETH${BE}ethereum")
val old2 = currencyIdValue("coin${BS}TRON${BE}tron")
val oldJson = "[$old1,$old2]"
val new1 = currencyIdValue("coin${BS}ethereum${BE}ethereum")
val new2 = currencyIdValue("coin${BS}tron${BE}tron")
val expected = "[$new1,$new2]"
val result = migrateLastSwapped(oldJson)
assertThat(result).isEqualTo(expected)
}
// endregion
// region migrate — preserves unrelated keys
@Test
fun `preserves unrelated preference keys`() = runTest {
val unrelatedKey = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY
val unrelatedValue = "some_value"
val prefs = mutablePreferencesOf(
PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to currencyIdJson("coin${BS}ETH${BE}ethereum"),
unrelatedKey to unrelatedValue,
)
val result = migration.migrate(prefs)
assertThat(result[unrelatedKey]).isEqualTo(unrelatedValue)
}
// endregion
// region helpers
private suspend fun migrateLastSwapped(json: String): String? {
val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to json)
val result = migration.migrate(prefs)
return result[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]
}
private suspend fun migrateSwapTransactions(json: String): String? {
val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to json)
val result = migration.migrate(prefs)
return result[PreferencesKeys.SWAP_TRANSACTIONS_KEY]
}
private fun currencyIdJson(id: String): String = "[${currencyIdValue(id)}]"
private fun currencyIdValue(id: String): String = "{\"cryptoCurrencyId\":\"$id\"}"
private companion object {
const val BS = '\u27E8' // ⟨ body start
const val BE = '\u27E9' // ⟩ body end
const val DP = '\u2192' // → derivation path delimiter
const val CA = '\u2693' // ⚓ contract address delimiter
}
// endregion
}

View file

@ -306,6 +306,7 @@
<string name="common_hour">Stunde</string>
<string name="common_import">Importieren</string>
<string name="common_in_progress">In Arbeit</string>
<string name="common_insufficient_balance">Unzureichende Mittel</string>
<string name="common_later">Später</string>
<string name="common_learn_more">Mehr erfahren</string>
<string name="common_left">%1$s übrig</string>
@ -421,6 +422,7 @@
<string name="custom_token_creation_error_token_already_exist_title">Token existiert bereits</string>
<string name="custom_token_creation_error_wrong_decimals">Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d</string>
<string name="custom_token_custom_derivation">Benutzerdefinierte Ableitung(derivation)</string>
<string name="custom_token_custom_derivation_dynamic_addresses_enabled_error">Die Funktion \"Dynamische Adressen\" ist aktiviert. Benutzerdefinierte \\"change\\" und \\"index\\" sind nicht verfügbar.</string>
<string name="custom_token_custom_derivation_placeholder">E. g. m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Benutzerdefinierte Ableitung (Derivation) eingeben</string>
<string name="custom_token_decimals_input_title">Dezimalstellen</string>
@ -473,6 +475,19 @@
<string name="domain_receive_assets_onboarding_description">Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust.</string>
<string name="domain_receive_assets_onboarding_network_name">%s Netzwerk</string>
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>
<string name="dynamic_addresses">Dynamische Adressen</string>
<string name="dynamic_addresses_custom_token_error_on_addition">Dynamische Adressen ist aktiviert. Die benutzerdefinierten Funktionen \"Ändern\" und \"Index\" sind nicht verfügbar.</string>
<string name="dynamic_addresses_enabled_toast_title">Einige Adressen fehlen</string>
<string name="dynamic_addresses_enter_features_privacy_description">Verwenden Sie für jede Transaktion eine neue Adresse, um die Rückverfolgbarkeit zu verringern und den Datenschutz in der Kette zu verbessern.</string>
<string name="dynamic_addresses_enter_features_privacy_title">Verbesserter Datenschutz</string>
<string name="dynamic_addresses_enter_features_receving_description">Einfacher Geldempfang in UTXO-basierten Netzwerken mit automatischer Adressgenerierung - keine manuelle Adressverwaltung erforderlich.</string>
<string name="dynamic_addresses_enter_features_receving_title">Nahtloser Empfang</string>
<string name="dynamic_addresses_enter_main_button_title">Dynamische Adressen aktivieren</string>
<string name="dynamic_addresses_enter_subtitle">Bei dynamischen Adressen wird jedes Mal eine neue Adresse erstellt, um den Datenschutz zu gewährleisten - Ihr Gesamtguthaben bleibt gleich. </string>
<string name="dynamic_addresses_error_has_custom_token_description">Dynamische Adressen können nicht aktiviert werden, da einige benutzerdefinierte Adressen/Tokens einen geänderten Ableitungspfad verwenden, der die erforderlichen Kriterien nicht erfüllt.</string>
<string name="dynamic_addresses_error_has_custom_token_title">Nicht verfügbar</string>
<string name="dynamic_addresses_error_service_unavailable_description">Wir können im Moment keine Verbindung zum Provider herstellen. Bitte versuchen Sie es später noch einmal.</string>
<string name="dynamic_addresses_error_service_unavailable_title">Der Dienst ist nicht verfügbar. Bitte versuchen Sie es erneut.</string>
<string name="earn_best_opportunities">Beste Gelegenheiten</string>
<string name="earn_clear_filter">Filter löschen</string>
<string name="earn_empty">Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein.</string>
@ -749,12 +764,18 @@
<string name="markets_insights_info_description_message">Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s</string>
<string name="markets_loading_error_title">Die Daten konnten nicht geladen werden...</string>
<string name="markets_loading_no_data_title">Keine Daten</string>
<string name="markets_portfolio_block_add_token_title">**Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen</string>
<string name="markets_portfolio_block_subtitle">Zum Portfolio hinzufügen</string>
<string name="markets_pulse_common_title">Marktimpuls</string>
<string name="markets_quick_actions">Schnelle Aktionen</string>
<string name="markets_search_clear_all_hints">Alles löschen</string>
<string name="markets_search_header_title">Markt durchsuchen</string>
<string name="markets_search_hint_header">Neueste</string>
<string name="markets_search_portfolio_header">In Ihrem Portfolio</string>
<string name="markets_search_result_title">Ergebnis</string>
<string name="markets_search_see_tokens_under_100k">Token unter 100k USD Marktkapitalisierung anzeigen</string>
<string name="markets_search_show_tokens">Token anzeigen</string>
<string name="markets_search_title_placeholder">Krypto, Nachrichten und mehr</string>
<string name="markets_search_token_no_result_title">Kein Ergebnis</string>
<string name="markets_select_network">Netzwerk auswählen</string>
<string name="markets_select_wallet">Wallet auswählen</string>
@ -827,9 +848,11 @@
<string name="markets_token_details_market_rating_description">Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung</string>
<string name="markets_token_details_market_rating_full">Marktposition</string>
<string name="markets_token_details_max_supply">Maximale Versorgung</string>
<string name="markets_token_details_max_supply_and_circulation_full">Zirkulation und maximale Versorgung</string>
<string name="markets_token_details_max_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
<string name="markets_token_details_max_supply_full">Maximale Versorgung</string>
<string name="markets_token_details_metrics">Metriken</string>
<string name="markets_token_details_metrics_no_limited">Nicht begrenzt</string>
<string name="markets_token_details_official_links">Offizielle Links</string>
<string name="markets_token_details_price_performance">Preisleistung</string>
<string name="markets_token_details_repository">Aufbewahrungsort</string>
@ -1104,10 +1127,14 @@
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
<string name="qr_scanner_camera_denied_title">Kamerazugriff verweigert</string>
<string name="qr_scanner_error_token_not_added_message">Der angeforderte Token wurde Ihrer Wallet nicht hinzugefügt. Bitte fügen Sie ihn hinzu und versuchen Sie es erneut.</string>
<string name="qr_scanner_error_token_not_added_title">Token nicht hinzugefügt</string>
<string name="qr_scanner_error_unrecognized_message">Dieser QR-Code konnte leider nicht erkannt werden.</string>
<string name="qr_scanner_error_unrecognized_title">Unerkannter QR-Code</string>
<string name="qr_scanner_error_unsupported_network_message">Dieses Netzwerk wird von keinem der von Ihnen hinzugefügten Token unterstützt. Fügen Sie einen unterstützten Token hinzu, um Krypto zu senden.</string>
<string name="qr_scanner_error_unsupported_network_title">Keine unterstützten Token gefunden</string>
<string name="qr_scanner_warning_unknown_parameters_message">Dieser QR-Code enthält Parameter, die nicht erkannt werden: %s. Einige Zahlungsdetails können verloren gehen, wenn Sie fortfahren.</string>
<string name="qr_scanner_warning_unknown_parameters_title">Unbekannte Parameter</string>
<string name="receive_bottom_sheet_no_memo_required_message">Kein Memo erforderlich</string>
<string name="receive_bottom_sheet_warning_message">%1$s ( %2$s ) im %3$s Netzwerk</string>
<string name="receive_bottom_sheet_warning_message_compact">%1$s im %2$s Netzwerk</string>
@ -1222,6 +1249,10 @@
<string name="send_memo">Memo: %s</string>
<string name="send_memo_destination_tag_error">Ungültiges Memo</string>
<string name="send_network_fee_warning_title">Abdeckung der Netzgebühren</string>
<plurals name="send_network_selection_hidden_tokens">
<item quantity="one">%d Token ist mit dieser Adresse nicht kompatibel</item>
<item quantity="other">%d -Tokens sind mit dieser Adresse nicht kompatibel</item>
</plurals>
<string name="send_nonce">Nonce</string>
<string name="send_nonce_footer">Eindeutige Nummer für jede Transaktion. Verwende diese Nummer, um eine ausstehende Transaktion erneut zu senden oder abzubrechen.</string>
<string name="send_nonce_hint">Nonce eingeben…</string>
@ -1489,14 +1520,18 @@
<string name="swapping_fee_estimation_error_text">Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support.</string>
<string name="swapping_from_title">Du wechselst</string>
<string name="swapping_high_price_impact_description">Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis.</string>
<string name="swapping_high_price_impact_text">Aufgrund geringer Liquidität erhalten Sie möglicherweise deutlich weniger. Versuchen Sie es mit einem kleineren Betrag oder einem anderen Anbieter.</string>
<string name="swapping_high_price_impact_title">Hoher Einfluss auf den Preis</string>
<string name="swapping_insufficient_funds">Unzureichende Mittel</string>
<string name="swapping_insufficient_funds_description">Nicht genügend Geldmittel, um diese Transaktion abzuschließen. Verringern Sie den zu erhaltenden Betrag oder fügen Sie weitere Mittel hinzu.</string>
<string name="swapping_permission_header">Erlaubnis erteilen</string>
<string name="swapping_swap_action">Tauschen</string>
<string name="swapping_swap_action_in_progress">Tauschen...</string>
<string name="swapping_to_title">Du erhältst</string>
<string name="swapping_token_list_title">Token auswählen</string>
<string name="swapping_token_not_available">Nicht verfügbar</string>
<string name="swapping_trade_too_large_text">Nicht genug Liquidität für diesen Handel. Reduzieren Sie den Betrag oder wählen Sie einen anderen Anbieter.</string>
<string name="swapping_trade_too_large_title">Handel zu groß</string>
<string name="tangem_pay_beta_notification_subtitle">Wir freuen uns über Ihr Feedback</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay jetzt in der Beta</string>
<string name="tangem_pay_card_frozen">Karte eingefroren</string>
@ -1567,7 +1602,7 @@
<string name="tangempay_card_details_receive_error_title">Empfangen ist jetzt nicht verfügbar</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 gegen eine Karte.</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_unfreeze_card">Karte entsperren</string>
<string name="tangempay_card_details_view_pin_code_description">Komm zurück zur App, falls du es vergisst.</string>
@ -1657,6 +1692,7 @@
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s Netzwerk</string>
<string name="token_details_unable_hide_alert_message">Der %1$s (%2$s) Token ist die Hauptwährung im %3$s Netzwerk und kann nicht versteckt werden, solange du andere Token dieses Netzwerks in der Liste aktiv hast.</string>
<string name="token_details_unable_hide_alert_title">%s kann nicht ausgeblendet werden</string>
<string name="token_market_metrics_no_data">N / A</string>
<string name="token_receive_show_qr_code_title">QR-Code anzeigen</string>
<string name="token_swap_changelly_promotion_message">Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s.</string>
<string name="token_swap_changelly_promotion_title">Tausche mit Changelly, %s Gebühren</string>
@ -1699,6 +1735,9 @@
<string name="universal_error">Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support.</string>
<string name="unlock_wallet_description_full">Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten.</string>
<string name="unsupported_wc_version">Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen.</string>
<string name="update_approval_permission_fee_note">Die vorherige Genehmigung wird widerrufen und eine neue erteilt. Das Netzwerk erhebt eine Token-Genehmigungsgebühr für jede dieser Aktionen. Als Beweis für den Widerruf wird in der Historie eine Transaktion mit einem Betrag von Null angezeigt.</string>
<string name="update_approval_permission_subtitle">Die Transaktion übersteigt den zuvor genehmigten Betrag.\nErlaubnis zum Fortfahren aktualisieren</string>
<string name="update_approval_permission_title">Erlaubnis zur Aktualisierung</string>
<string name="upgrade_to_hardware_wallet_button_title">Upgrade auf Hardware-Wallet</string>
<string name="user_push_notification_agreement_argument_one">Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten</string>
<string name="user_push_notification_agreement_argument_one_subtitle">Echtzeit-Warnungen für Transaktionen, Umtausch und wichtige Aktualisierungen.</string>

View file

@ -161,7 +161,7 @@
<string name="biometric_updated_warning_title">Se requiere atención</string>
<string name="bitcoin_promo_activation_error">Se produjo un error al procesar tu código promocional. Inténtalo de nuevo más tarde.</string>
<string name="bitcoin_promo_activation_error_title">Error de activación</string>
<string name="bitcoin_promo_activation_success">Tu código promocional se activó correctamente. Una recompensa se acreditará en tu cuenta dentro de 14 días.</string>
<string name="bitcoin_promo_activation_success">Su código promocional se activó correctamente. Se acreditará la recompensa en su cuenta dentro de 14 días.</string>
<string name="bitcoin_promo_activation_success_title">Código promocional activado</string>
<string name="bitcoin_promo_already_activated">Este código promocional ya ha sido utilizado y no puede activarse de nuevo.</string>
<string name="bitcoin_promo_already_activated_title">Código no disponible</string>
@ -306,6 +306,7 @@
<string name="common_hour">hora</string>
<string name="common_import">Importe</string>
<string name="common_in_progress">En progreso</string>
<string name="common_insufficient_balance">Fondos insuficientes</string>
<string name="common_later">Más tarde</string>
<string name="common_learn_more">Más información</string>
<string name="common_left">%1$s quedan</string>
@ -326,6 +327,7 @@
<string name="common_nft">NFT</string>
<string name="common_no">No</string>
<string name="common_no_address">Ninguna dirección</string>
<string name="common_no_results">Sin resultados</string>
<string name="common_not_added">No agregadas</string>
<string name="common_not_available">No disponible</string>
<string name="common_not_now">Ahora no</string>
@ -384,6 +386,7 @@
<string name="common_to">A</string>
<string name="common_to_wallet_name">A %s</string>
<string name="common_today">Hoy</string>
<string name="common_token_send">Token para enviar</string>
<plurals name="common_tokens_count">
<item quantity="one">%d token</item>
<item quantity="other">%d tokens</item>
@ -396,6 +399,7 @@
<string name="common_understand">Entiendo</string>
<string name="common_understand_continue">Entiendo, continuar</string>
<string name="common_unknown_error">Hubo un error. Por favor inténtelo de nuevo.</string>
<string name="common_unlock">Desbloquear</string>
<string name="common_unreachable">Inaccesible</string>
<string name="common_unstake">Termine el staking</string>
<string name="common_utxo_validate_withdrawal_message_warning">Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad.</string>
@ -405,7 +409,7 @@
<string name="common_week">semana</string>
<string name="common_with">con</string>
<string name="common_yes"></string>
<string name="common_yield_mode">Modo de rendimiento</string>
<string name="common_yield_mode">Modo de Rendimiento</string>
<string name="contract_address_copied_message">Dirección de contrato copiada</string>
<string name="currency_subtitle_expanded">Redes disponibles</string>
<string name="custom_token_another_account_dialog_description">La derivación de su token coincide con la derivación de %1$s. Su token se añadirá a esta cuenta.</string>
@ -418,6 +422,7 @@
<string name="custom_token_creation_error_token_already_exist_title">Este token ya existe</string>
<string name="custom_token_creation_error_wrong_decimals">Los decimales deben ser un número entero válido, hasta %d</string>
<string name="custom_token_custom_derivation">Derivación personalizada</string>
<string name="custom_token_custom_derivation_dynamic_addresses_enabled_error">La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \\"cambiar\\" e \\"índice\\" no están disponibles.</string>
<string name="custom_token_custom_derivation_placeholder">Por ejemplo m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Introduzca una derivación personalizada</string>
<string name="custom_token_decimals_input_title">Decimales</string>
@ -460,7 +465,7 @@
<string name="details_row_title_signed_hashes">Firmado</string>
<string name="details_send_feedback">Enviar comentarios</string>
<string name="details_title">Detalles</string>
<string name="details_wallets_section_description">Puedes tener solo una billetera móvil a la vez. Puede actualizarse a una billetera fría Tangem o usarse junto con una nueva billetera fría.</string>
<string name="details_wallets_section_description">Puede tener solo una billetera móvil. Puede actualizarse a una billetera fría Tangem o añadir una billetera de hardware nueva.</string>
<string name="disclaimer_error_loading">Compruebe su conexión a internet o cambia a una red diferente</string>
<string name="disclaimer_title">Condiciones de uso</string>
<string name="domain_receive_assets_default_address">Dirección por defecto</string>
@ -470,6 +475,19 @@
<string name="domain_receive_assets_onboarding_description">Enviar activos a otras redes resultará en una pérdida permanente.</string>
<string name="domain_receive_assets_onboarding_network_name">Red de %s</string>
<string name="domain_receive_assets_onboarding_title">Envíe fondos utilizando solo</string>
<string name="dynamic_addresses">Direcciones dinámicas</string>
<string name="dynamic_addresses_custom_token_error_on_addition">La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \"cambiar\" e \"índice\" no están disponibles.</string>
<string name="dynamic_addresses_enabled_toast_title">Direcciones dinámicas habilitadas</string>
<string name="dynamic_addresses_enter_features_privacy_description">Utilice una nueva dirección para cada transacción para reducir la trazabilidad y mejorar la privacidad on-chain.</string>
<string name="dynamic_addresses_enter_features_privacy_title">Privacidad mejorada</string>
<string name="dynamic_addresses_enter_features_receving_description">Reciba fondos fácilmente en redes basadas en UTXO con generación automática de direcciones, sin necesidad de gestión manual de direcciones.</string>
<string name="dynamic_addresses_enter_features_receving_title">Recepción sin problemas</string>
<string name="dynamic_addresses_enter_main_button_title">Habilitar direcciones dinámicas</string>
<string name="dynamic_addresses_enter_subtitle">Las direcciones dinámicas crean una nueva cada vez para mayor privacidad - su saldo total no varía. </string>
<string name="dynamic_addresses_error_has_custom_token_description">Las direcciones dinámicas no pueden activarse porque algunas direcciones/tokens personalizadas utilizan una ruta de derivación modificada, que no cumple los criterios requeridos.</string>
<string name="dynamic_addresses_error_has_custom_token_title">Direcciones dinámicas no disponibles</string>
<string name="dynamic_addresses_error_service_unavailable_description">No podemos conectar con el proveedor en este momento. Vuelva a intentarlo más tarde.</string>
<string name="dynamic_addresses_error_service_unavailable_title">Servicio no disponible. Por favor, inténtelo de nuevo.</string>
<string name="earn_best_opportunities">Las mejores oportunidades</string>
<string name="earn_clear_filter">Limpiar filtro</string>
<string name="earn_empty">La lista está temporalmente vacía porque se está actualizando. Vuelva a consultarla en un momento.</string>
@ -478,9 +496,9 @@
<string name="earn_filter_by">Filtrar por</string>
<string name="earn_filter_my_networks">Mis redes</string>
<string name="earn_filter_networks">Redes</string>
<string name="earn_mostly_used">Mayormente usado</string>
<string name="earn_mostly_used">Popular</string>
<string name="earn_no_results">Sin resultados</string>
<string name="earn_title">Ganar</string>
<string name="earn_title">Modo Staking &amp; Rendimiento</string>
<string name="email_preface_wc_error">Hola equipo de soporte, he encontrado un error con el código: %s</string>
<string name="email_subject_wc_error">Error de WalletConnect</string>
<string name="error_wrong_wallet_tapped">Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera</string>
@ -653,7 +671,7 @@
<string name="hw_remove_wallet_confirmation_title">¿Está seguro que desea hacer esto?</string>
<string name="hw_remove_wallet_nav_title">Olvidar billetera</string>
<string name="hw_remove_wallet_notification_action_backup_go">Ir a copia de seguridad</string>
<string name="hw_remove_wallet_notification_action_backup_view">Ver copia de seguridad</string>
<string name="hw_remove_wallet_notification_action_backup_view">Ver frase de recuperación</string>
<string name="hw_remove_wallet_notification_action_forget">Olvidar la billetera</string>
<string name="hw_remove_wallet_notification_action_forget_anyway">Olvidar de todos modos</string>
<string name="hw_remove_wallet_notification_description_has_backup">Esta billetera tiene una copia de seguridad. Asegúrese de poder recuperarla antes de olvidarla.</string>
@ -689,6 +707,9 @@
<string name="initial_message_scan_header">Acerque para escanear</string>
<string name="initial_message_sign_header">Toque para firmar</string>
<string name="initial_message_tap_header">Coloque la tarjeta o el anillo</string>
<string name="initial_wallet_sync_banner_description">Su billetera está sincronizada y lista. \n Faltan algunos tokens?</string>
<string name="initial_wallet_sync_banner_title">Billetera importada correctamente</string>
<string name="initial_wallet_sync_restore_progress">Restaurando %d%%</string>
<string name="key_invalidated_warning_description">Ha actualizado la biometría, escanee su tarjeta o anillo para entrar</string>
<string name="koinos_insufficient_balance_to_send_koin_description">Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia</string>
<string name="koinos_insufficient_balance_to_send_koin_title">Saldo insuficiente</string>
@ -700,6 +721,7 @@
<string name="koinos_mana_level_title">Nivel de Mana</string>
<string name="main_empty_tokens_list_message">Para hacer un seguimiento de sus criptomonedas y transacciones, agregue tokens</string>
<string name="main_manage_tokens">Gestionar tokens</string>
<string name="main_qr_scan_hint">Escanee el código QR para enviar fondos o conectarse a una aplicación</string>
<string name="main_scan_card_warning_view_subtitle">Para acceder a todas las redes necesita escanear la tarjeta</string>
<string name="main_scan_card_warning_view_title">Escanee su tarjeta o anillo</string>
<string name="main_swap_changelly_promotion_message">Disfrute de %1$s comisiones de servicio en los swaps a través de Changelly a partir de febrero %2$s-%3$s</string>
@ -734,15 +756,22 @@
<string name="markets_apy_placeholder">APY %s</string>
<string name="markets_common_my_portfolio">Mi portafolio</string>
<string name="markets_common_title">Mercado</string>
<string name="markets_earn_common_title">Gane con Tangem</string>
<string name="markets_earn_common_title">Modo Staking &amp; Rendimiento</string>
<string name="markets_generate_addresses_notification">Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem</string>
<string name="markets_hint">Para agregar tokens, abra esta página o pulse sobre la barra de búsqueda</string>
<string name="markets_hint_part_one">Deslice el dedo hacia arriba para explorar el mercado</string>
<string name="markets_hint_part_two">Descubre nuevas gemas ocultas</string>
<string name="markets_insights_info_description_message">Los datos de este apartado proceden de las siguientes redes: %s</string>
<string name="markets_loading_error_title">No se pueden cargar los datos…</string>
<string name="markets_loading_no_data_title">Sin datos</string>
<string name="markets_portfolio_block_add_token_title">**Añadir a su portafolio** para empezar a comprar, intercambiar o recibir este activo</string>
<string name="markets_portfolio_block_subtitle">En su portafolio</string>
<string name="markets_pulse_common_title">Análisis del Mercado</string>
<string name="markets_quick_actions">Acciones rápidas</string>
<string name="markets_search_clear_all_hints">Borrar todo</string>
<string name="markets_search_header_title">Buscar tokens</string>
<string name="markets_search_hint_header">Recientes</string>
<string name="markets_search_portfolio_header">En su portafolio</string>
<string name="markets_search_result_title">Resultado</string>
<string name="markets_search_see_tokens_under_100k">Ver tokens con marketcap inferior a 100.000$</string>
<string name="markets_search_show_tokens">Mostrar tokens</string>
@ -799,6 +828,10 @@
<string name="markets_token_details_holders">Titulares</string>
<string name="markets_token_details_holders_description">El cambio en el número de poseedores de tokens dentro de un período de tiempo específico</string>
<string name="markets_token_details_holders_full">Titulares</string>
<string name="markets_token_details_insight_day_timeline">D</string>
<string name="markets_token_details_insight_month_timeline">M</string>
<string name="markets_token_details_insight_week_timeline">S</string>
<string name="markets_token_details_insight_year_timeline">A</string>
<string name="markets_token_details_insights">Ideas</string>
<string name="markets_token_details_links">Enlaces</string>
<string name="markets_token_details_liquidity">Liquidez</string>
@ -814,9 +847,11 @@
<string name="markets_token_details_market_rating_description">Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado</string>
<string name="markets_token_details_market_rating_full">Evaluación de mercado</string>
<string name="markets_token_details_max_supply">Suministro máximo</string>
<string name="markets_token_details_max_supply_and_circulation_full">Suministro circulante y máximo</string>
<string name="markets_token_details_max_supply_description">La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular</string>
<string name="markets_token_details_max_supply_full">Suministro máximo</string>
<string name="markets_token_details_metrics">Métrica</string>
<string name="markets_token_details_metrics_no_limited">Sin limitación</string>
<string name="markets_token_details_official_links">Enlaces oficiales</string>
<string name="markets_token_details_price_performance">Rendimiento de precios</string>
<string name="markets_token_details_repository">Repositorio</string>
@ -826,9 +861,11 @@
<string name="markets_token_details_total_supply">Suministro total</string>
<string name="markets_token_details_total_supply_description">La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular</string>
<string name="markets_token_details_total_supply_full">Suministro total</string>
<string name="markets_token_details_trading_interval">24h</string>
<string name="markets_token_details_trading_volume">Volumen de operaciones (24 horas)</string>
<string name="markets_token_details_trading_volume_24h_description">La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado.</string>
<string name="markets_token_details_trading_volume_full">Volumen de operaciones (24 horas)</string>
<string name="markets_token_details_valuation_value_in_total">%s en total</string>
<string name="markets_token_details_volume">Volumen</string>
<string name="markets_tooltip_message">Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado</string>
<string name="markets_tooltip_title">Agregar tokens</string>
@ -852,6 +889,7 @@
<string name="news_related_tokens">Tokens relacionados</string>
<string name="news_sources">Noticias relacionadas</string>
<string name="news_stay_in_the_loop">Manténgase informado</string>
<string name="news_trending_score">Puntuación de tendencia</string>
<string name="nfc_error_unavailable">NFC no está disponible en su dispositivo</string>
<string name="nft_about_title">Acerca de NFT</string>
<string name="nft_asset">Activo NFT</string>
@ -909,8 +947,8 @@
<string name="notification_black_friday_text">Obtén $10 en BTC con cada billetera \n ¡Date prisa!</string>
<string name="notification_black_friday_title">Black Friday: hasta 30% DESCUENTO</string>
<string name="notification_one_plus_one_button">Vamos</string>
<string name="notification_one_plus_one_text">Crea el par perfecto de packs de Tangem. Por tiempo limitado.</string>
<string name="notification_one_plus_one_title">1+1: Compra una billetera y obtén un 50% dto. en la segunda</string>
<string name="notification_one_plus_one_text">¡Tiempo limitado!</string>
<string name="notification_one_plus_one_title">1+1: Compra una billetera y obtenga un 50% dto. en la segunda</string>
<string name="notification_referral_promo_button">Únase ahora</string>
<string name="notification_referral_promo_text">Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento.</string>
<string name="notification_referral_promo_title">¡Obtenga RECOMPENSAS por cada amigo!</string>
@ -1088,6 +1126,14 @@
<string name="qr_scanner_camera_denied_settings_button">Ajustes</string>
<string name="qr_scanner_camera_denied_text">No ha dado acceso a su cámara</string>
<string name="qr_scanner_camera_denied_title">Acceso a la cámara denegado</string>
<string name="qr_scanner_error_token_not_added_message">El token solicitado no se ha añadido a su billetera. Por favor, añádalo e inténtelo de nuevo.</string>
<string name="qr_scanner_error_token_not_added_title">Token no añadido</string>
<string name="qr_scanner_error_unrecognized_message">Lo sentimos, no se ha podido reconocer este código QR.</string>
<string name="qr_scanner_error_unrecognized_title">Código QR no reconocido</string>
<string name="qr_scanner_error_unsupported_network_message">Esta red no es compatible con ninguno de sus tokens añadidos. Añada un token compatible para enviar cripto.</string>
<string name="qr_scanner_error_unsupported_network_title">No se encontraron tokens compatibles</string>
<string name="qr_scanner_warning_unknown_parameters_message">Este código QR contiene parámetros que no son reconocidos: %s. Si continúa, es posible que se pierdan algunos detalles de pago.</string>
<string name="qr_scanner_warning_unknown_parameters_title">Parámetros desconocidos</string>
<string name="receive_bottom_sheet_no_memo_required_message">No se requiere nota</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) en la red %3$s</string>
<string name="receive_bottom_sheet_warning_message_compact">%1$s en la red %2$s</string>
@ -1164,7 +1210,7 @@
<string name="send_alert_fee_too_low_text">Ha especificado una comisión inferior a la cantidad recomendada, lo que podría provocar un retraso en su transacción. ¿Continuar?</string>
<string name="send_alert_transaction_failed_text">Razón: %1$s\nCódigo: %2$s</string>
<string name="send_alert_transaction_failed_title">La transacción está incompleta</string>
<string name="send_amount_convert_to_another_token">Convertir a otro token</string>
<string name="send_amount_convert_to_another_token">Cambiar a otro token o red</string>
<string name="send_amount_label">Montante</string>
<string name="send_amount_receive_token_subtitle">Se enviará al destinatario</string>
<string name="send_bitcoin_custom_fee_footer">Puede establecer su tarifa de transacción ajustando el valor en el campo Satoshi por vByte.</string>
@ -1202,6 +1248,10 @@
<string name="send_memo">Memo: %s</string>
<string name="send_memo_destination_tag_error">Memo no válido</string>
<string name="send_network_fee_warning_title">Cobertura de tarifa de red</string>
<plurals name="send_network_selection_hidden_tokens">
<item quantity="one">El token %d no es compatible con esta dirección</item>
<item quantity="other">Los tokens %d no son compatibles con esta dirección</item>
</plurals>
<string name="send_nonce">Nonce</string>
<string name="send_nonce_footer">Número único para cada transacción. Utilícelo para reenviar o cancelar una transacción pendiente.</string>
<string name="send_nonce_hint">Introduzca nonce...</string>
@ -1229,6 +1279,11 @@
<string name="send_notification_transaction_limit_title">Limitación de transacciones</string>
<string name="send_optional_field">Opcional</string>
<string name="send_qrcode_scan_info">Por favor, alinee su código QR con el cuadrado para escanearlo. Asegúrese de escanear la dirección de la red %s.</string>
<string name="send_rate_fixed_info_description">Al utilizar un tipo de cambio fijo, el importe que recibe queda garantizado en el momento de la operación. Esto le protege de las fluctuaciones de precio durante la transacción.</string>
<string name="send_rate_fixed_info_title">Tasa fija</string>
<string name="send_rate_floating_info_description">Un tipo variable significa que la cantidad final que recibe puede variar ligeramente en función de las condiciones del mercado entre el momento en que inicia y finaliza el swap.</string>
<string name="send_rate_floating_info_title">Tasa flotante</string>
<string name="send_rate_is_fixed">La tasa es fija</string>
<string name="send_recent_transactions">Reciente</string>
<string name="send_recipient">Destinatario</string>
<string name="send_recipient_address_error">No es una dirección válida</string>
@ -1272,6 +1327,7 @@
<string name="send_with_swap_recipient_get_amount">El destinatario recibe %s</string>
<string name="send_with_swap_remove_convert_alert_message">¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores.</string>
<string name="send_with_swap_remove_convert_alert_title">Eliminar conversión</string>
<string name="send_with_swap_something_went_wrong">Algo salió mal. Inténtelo de nuevo.</string>
<string name="send_with_swap_title">Enviar con swap</string>
<string name="sent_transaction_sent_title">Transacción enviada</string>
<string name="settings_card_settings_footer">Escanee la tarjeta/anillo que quiere configurar</string>
@ -1463,14 +1519,18 @@
<string name="swapping_fee_estimation_error_text">Error en la estimación de la tarifa. Envíe sus comentarios al servicio de asistencia.</string>
<string name="swapping_from_title">Usted intercambia</string>
<string name="swapping_high_price_impact_description">Hacer un intercambio de esta cantidad del token seleccionado causará un impacto significativo en el precio y reducirá su resultado.</string>
<string name="swapping_high_price_impact_text">Es posible que reciba una cantidad significativamente menor debido a la baja liquidez. Pruebe con una cantidad menor o con otro proveedor.</string>
<string name="swapping_high_price_impact_title">Alto impacto en los precios</string>
<string name="swapping_insufficient_funds">Fondos insuficientes</string>
<string name="swapping_insufficient_funds_description">No hay fondos suficientes para completar esta transacción. Reduzca el importe a recibir o añada más fondos.</string>
<string name="swapping_permission_header">Dar autorización</string>
<string name="swapping_swap_action">Intercambiar</string>
<string name="swapping_swap_action_in_progress">Intercambiando...</string>
<string name="swapping_to_title">Usted recibe</string>
<string name="swapping_token_list_title">Elige token</string>
<string name="swapping_token_not_available">no disponible</string>
<string name="swapping_trade_too_large_text">No hay liquidez suficiente para esta operación. Reduzca el importe o elija otro proveedor.</string>
<string name="swapping_trade_too_large_title">Operación demasiado grande</string>
<string name="tangem_pay_beta_notification_subtitle">Nos encantaría recibir tus comentarios</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay ya está en beta</string>
<string name="tangem_pay_card_frozen">Tarjeta congelada</string>
@ -1509,6 +1569,7 @@
<string name="tangempay_cancel_kyc">Ocultar verificación de la pantalla</string>
<string name="tangempay_card_details_add_funds">Agregar fondos</string>
<string name="tangempay_card_details_add_funds_subtitle">Opciones de recarga</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Añadir a Google Wallet</string>
<string name="tangempay_card_details_card_number">Número de tarjeta</string>
<string name="tangempay_card_details_change_pin">Modificar PIN</string>
<string name="tangempay_card_details_change_pin_success_description">La tarjeta está completamente lista para pagos.</string>
@ -1596,7 +1657,7 @@
<string name="tangempay_sync_needed">Sesión expirada</string>
<string name="tangempay_sync_needed_restore_access">Restablecer acceso</string>
<string name="tangempay_tangem_visa_card">Usa USDC para pagos cotidianos</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay temporalmente no disponible</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</string>
<string name="tangempay_title">Tangem Pay</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">Tu saldo USDC Polygon on-chain difiere de tu saldo de tarjeta y se actualiza dentro de 2 días hábiles después de una compra. Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras.</string>
@ -1630,6 +1691,7 @@
<string name="token_details_token_type_subtitle_no_standard">Token en la red %%image%% %1$s</string>
<string name="token_details_unable_hide_alert_message">El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista</string>
<string name="token_details_unable_hide_alert_title">No se puede ocultar %s</string>
<string name="token_market_metrics_no_data">N/A</string>
<string name="token_receive_show_qr_code_title">Mostrar código QR</string>
<string name="token_swap_changelly_promotion_message">Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero.</string>
<string name="token_swap_changelly_promotion_title">Intercambie con Changelly, %s comisiones</string>
@ -1672,6 +1734,9 @@
<string name="universal_error">Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte.</string>
<string name="unlock_wallet_description_full">Use %s o escanee una tarjeta/anillo para tener acceso a su billetera</string>
<string name="unsupported_wc_version">Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente.</string>
<string name="update_approval_permission_fee_note">Se revocará el permiso anterior y se emitirá uno nuevo. La red cobrará una tarifa de aprobación de tokens por cada una de estas acciones. Verá una transacción de importe cero en el historial como comprobante de la revocación.</string>
<string name="update_approval_permission_subtitle">La transacción excede el límite de permisos otorgados previamente.\nActualice los permisos para continuar.</string>
<string name="update_approval_permission_title">Actualizar permisos</string>
<string name="upgrade_to_hardware_wallet_button_title">Actualización a billetera de hardware</string>
<string name="user_push_notification_agreement_argument_one">Manténgase actualizado con las últimas funciones y noticias</string>
<string name="user_push_notification_agreement_argument_one_subtitle">Alertas en tiempo real de transacciones, intercambios y actualizaciones críticas.</string>
@ -1842,7 +1907,7 @@
<string name="wallet_settings_access_code_description">Este código de acceso protege su billetera y se utiliza para iniciar sesión y firmar transacciones.</string>
<string name="wallet_settings_access_code_title">Establecer/Cambiar código de acceso</string>
<string name="wallet_settings_change_access_code_title">Cambiar código de acceso</string>
<string name="wallet_settings_push_notifications_description">Manténgase informado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem.</string>
<string name="wallet_settings_push_notifications_description">Reciba notificaciones push para las transacciones entrantes en su billetera.</string>
<string name="wallet_settings_push_notifications_huawei_warning">Es posible que las notificaciones push no funcionen actualmente en dispositivos Huawei. Estamos trabajando activamente en una solución y la publicaremos en una próxima actualización. ¡Gracias por su comprensión!</string>
<string name="wallet_settings_push_notifications_title">Notificaciones de transacciones</string>
<string name="wallet_settings_set_access_code_title">Establecer código de acceso</string>
@ -1870,7 +1935,7 @@
<string name="warning_clore_migration_message">Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento.</string>
<string name="warning_clore_migration_message_label">Mensaje</string>
<string name="warning_clore_migration_open_portal_button">Abrir el portal de reclamaciones</string>
<string name="warning_clore_migration_sheet_description">Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore.</string>
<string name="warning_clore_migration_sheet_description">Para continuar utilizando sus tokens de Clore, debe completar la migración de tokens de acuerdo con la información del Portal de Reclamación.</string>
<string name="warning_clore_migration_sheet_title">Migración de la red Clore</string>
<string name="warning_clore_migration_sign_button">Firmar</string>
<string name="warning_clore_migration_signature_label">Firma</string>
@ -1925,7 +1990,7 @@
<string name="warning_network_unreachable_message">La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde.</string>
<string name="warning_network_unreachable_title">La red no está disponible</string>
<string name="warning_no_account_title">Recargue su billetera</string>
<string name="warning_no_backup_message">Su billetera no ha sido respaldado. Realice este procedimiento para proteger sus activos ahora.</string>
<string name="warning_no_backup_message">Su billetera no tiene aún una copia de seguridad. Hágala ahora para proteger sus activos.</string>
<string name="warning_no_backup_title">Falta backup</string>
<string name="warning_number_of_signed_hashes_incorrect_message">Esta tarjeta se ha utilizado previamente para transacciones. Si la recibió de una fuente no confiable, considere retirar todos los fondos. Si es su tarjeta, no se requiere ninguna acción.</string>
<string name="warning_number_of_signed_hashes_incorrect_title">La tarjeta ya ha firmado transacciones</string>
@ -2099,7 +2164,7 @@
<string name="yield_module_earn_sheet_high_fee_description">La tasa de red es actualmente demasiado alta para ejecutar préstamos. Los fondos se suministrarán una vez que baje a %1$s o menos.</string>
<string name="yield_module_earn_sheet_my_funds_title">Mis fondos</string>
<string name="yield_module_earn_sheet_provider_description">Su %1$s está ahora depositado en Aave y devenga intereses. Tiene un token%2$s, que representa su saldo y crece con el tiempo. Cuando deposite más fondos, estos se suministrarán a Aave para ganar intereses, menos una comisión por transacción.</string>
<string name="yield_module_earn_sheet_title">Modo de rendimiento</string>
<string name="yield_module_earn_sheet_title">Modo Rendimiento</string>
<string name="yield_module_earn_sheet_total_earnings_title">Ganancias totales</string>
<string name="yield_module_earn_sheet_transfers_title">Transferencias a Aave</string>
<string name="yield_module_explore_sheet_explore_aave_button_title">Explore Aave</string>
@ -2111,7 +2176,7 @@
<string name="yield_module_fee_policy_sheet_max_fee_title">Tarifa máxima</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">El monto mínimo se calcula en función de la comisión de la red actual para que no supere el 4%% del importe de la recarga, lo que hace que el mínimo sea %1$s (%2$s).</string>
<string name="yield_module_fee_policy_sheet_min_amount_title">Recarga mínima</string>
<string name="yield_module_fee_policy_sheet_title">Política de tarifas</string>
<string name="yield_module_fee_policy_sheet_title">Política de tarifas por recarga</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem también cobra una comisión de servicio del 15% sobre el rendimiento obtenido.</string>
<string name="yield_module_high_fee_error">Sus fondos se suministrarán automáticamente a Aave una vez que las comisiones de red sean más bajas o su saldo alcance el importe mínimo requerido.</string>
<string name="yield_module_high_network_fees_notification_description">Las comisiones son más altas de lo habitual debido a la alta actividad del mercado. Puede continuar ahora o volver a consultar más tarde cuando las comisiones sean más bajas.</string>
@ -2131,7 +2196,7 @@
<string name="yield_module_promo_screen_self_custodial_subtitle">Aave es un protocolo on-chain para crear mercados de liquidez no custodiales y ganar intereses a tasa variable.</string>
<string name="yield_module_promo_screen_self_custodial_title">Descentralizado y autocustodiado</string>
<string name="yield_module_promo_screen_terms_disclaimer">Al utilizar este servicio, usted acepta que el proveedor\n%1$s y %2$s</string>
<string name="yield_module_promo_screen_title">Conectar Aave</string>
<string name="yield_module_promo_screen_title">Conectar con Aave</string>
<string name="yield_module_promo_screen_title_v2">Consiga un %1$s%% APY\nen su saldo</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave %1$s%% - Tipo de interés variable</string>
<string name="yield_module_promo_screen_variable_rate_info_v2">Tipo de interés variable</string>
@ -2144,7 +2209,7 @@
<string name="yield_module_receive_sheet_description">Cuando recargue, sus fondos se enviarán automáticamente a Aave para comenzar a ganar intereses. %s se deducirá para cubrir la tarifa de transacción.</string>
<string name="yield_module_start_earning">Suministro de activos</string>
<string name="yield_module_start_earning_sheet_description">Su %s se suministrará a Aave, pero seguirá siendo gestionable.</string>
<string name="yield_module_start_earning_sheet_fee_policy">Ver política de tarifas</string>
<string name="yield_module_start_earning_sheet_fee_policy">Ver política de tarifas por recarga</string>
<string name="yield_module_start_earning_sheet_next_deposits">Sus próximas recargas se suministrarán automáticamente a Aave.</string>
<string name="yield_module_start_earning_sheet_next_deposits_v2">Todos tus futuros depósitos %1$s se suministrarán automáticamente a Aave.</string>
<string name="yield_module_status_active">Activo</string>
@ -2158,8 +2223,8 @@
<string name="yield_module_token_details_earn_notification_apy">APY</string>
<string name="yield_module_token_details_earn_notification_description">Los intereses se devengan automáticamente</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_subtitle">Los intereses se devengan automáticamente</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Modo de rendimiento</string>
<string name="yield_module_token_details_earn_notification_processing">Procesando su depósito</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Modo Rendimiento</string>
<string name="yield_module_token_details_earn_notification_processing">Activando Modo Rendimiento</string>
<string name="yield_module_token_details_earn_notification_title">Modo Rendimiento</string>
<string name="yield_module_transaction_deploy_contract">Implementación del contrato del Modo Rendimiento</string>
<string name="yield_module_transaction_enter">Modo Rendimiento activado</string>

View file

@ -1086,6 +1086,14 @@
<string name="qr_scanner_camera_denied_settings_button">Paramètres</string>
<string name="qr_scanner_camera_denied_text">Vous n\'avez pas donné accès à votre caméra</string>
<string name="qr_scanner_camera_denied_title">Accès à la caméra refusé</string>
<string name="qr_scanner_error_token_not_added_message">Le jeton demandé n\'est pas ajouté à votre portefeuille. Veuillez l\'ajouter, puis réessayez.</string>
<string name="qr_scanner_error_token_not_added_title">Le jeton n\'a pas été ajouté</string>
<string name="qr_scanner_error_unrecognized_message">Désolé, ce code QR n\'a pas pu être reconnu.</string>
<string name="qr_scanner_error_unrecognized_title">Code QR non reconnu</string>
<string name="qr_scanner_error_unsupported_network_message">Ce réseau n\'est pris en charge par aucun des jetons que vous avez ajoutés. Ajoutez un jeton pris en charge pour envoyer des cryptomonnaies.</string>
<string name="qr_scanner_error_unsupported_network_title">Aucun jeton pris en charge n\'a été trouvé</string>
<string name="qr_scanner_warning_unknown_parameters_message">Ce code QR contient des paramètres non reconnus : %s. Si vous continuez, certaines informations de paiement risquent d\'être perdues.</string>
<string name="qr_scanner_warning_unknown_parameters_title">Paramètres inconnus</string>
<string name="receive_bottom_sheet_no_memo_required_message">Aucun mémo requis</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) sur le réseau %3$s</string>
<string name="receive_bottom_sheet_warning_message_compact">%1$s sur le réseau %2$s</string>

View file

@ -415,6 +415,7 @@
<string name="custom_token_creation_error_token_already_exist_title">トークンはすでに存在します</string>
<string name="custom_token_creation_error_wrong_decimals">小数は%dまでの有効な整数である必要があります</string>
<string name="custom_token_custom_derivation">カスタム派生パス</string>
<string name="custom_token_custom_derivation_dynamic_addresses_enabled_error">動的アドレスが有効です。カスタムの「change」と「index」は利用できません。</string>
<string name="custom_token_custom_derivation_placeholder">m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">カスタム派生パスを入力</string>
<string name="custom_token_decimals_input_title">小数</string>
@ -756,11 +757,13 @@
<string name="markets_insights_info_description_message">このセクションのデータは、次のネットワークから取得されています: %s</string>
<string name="markets_loading_error_title">データを読み込めません…</string>
<string name="markets_loading_no_data_title">データなし</string>
<string name="markets_portfolio_block_add_token_title">**ポートフォリオに追加して**、この資産の買付・交換・受け取りを始めましょう</string>
<string name="markets_portfolio_block_subtitle">ポートフォリオ内</string>
<string name="markets_pulse_common_title">マーケット動向</string>
<string name="markets_quick_actions">クイックアクション</string>
<string name="markets_search_clear_all_hints">すべてクリア</string>
<string name="markets_search_header_title">トークンを探す</string>
<string name="markets_search_hint_header">最近</string>
<string name="markets_search_hint_header">最近の検索</string>
<string name="markets_search_portfolio_header">ポートフォリオ内</string>
<string name="markets_search_result_title">結果</string>
<string name="markets_search_see_tokens_under_100k">時価総額10万ドル以下のトークンを見る</string>
@ -1712,6 +1715,9 @@
<string name="universal_error">エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。</string>
<string name="unlock_wallet_description_full">%sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください</string>
<string name="unsupported_wc_version">接続に失敗しましたこのdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。</string>
<string name="update_approval_permission_fee_note">以前の承認は取り消され、新しい承認が発行されます。これらの各操作には、ネットワークによるトークン承認手数料がかかります。履歴には、取り消しの証拠として金額0の取引が表示されます。</string>
<string name="update_approval_permission_subtitle">取引金額が、以前に許可された承認額を超えています。\n続行するには、承認内容を更新してください。</string>
<string name="update_approval_permission_title">承認を更新</string>
<string name="upgrade_to_hardware_wallet_button_title">ハードウェアウォレットにアップグレード</string>
<string name="user_push_notification_agreement_argument_one">最新の機能とニュースをお届けします</string>
<string name="user_push_notification_agreement_argument_one_subtitle">取引・スワップ・重要な更新に関するリアルタイム通知。</string>

View file

@ -80,6 +80,10 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Adicionar tokens</string>
<string name="action_buttons_you_want_to_receive">Escolha o token que deseja receber.</string>
<string name="action_buttons_you_want_to_swap">Escolha o token que deseja trocar.</string>
<string name="add_and_manage_sheet_manage_subtitle">Adicione ao seu portfólio</string>
<string name="add_and_manage_sheet_manage_title">Adicionar tokens</string>
<string name="add_and_manage_sheet_organize_subtitle">Classificação e agrupamento</string>
<string name="add_and_manage_sheet_organize_title">Organizar tokens</string>
<string name="add_custom_token_choose_network">Escolha a rede</string>
<string name="add_custom_token_title">Adicionar token personalizado</string>
<string name="add_tokens_title">Gerenciar tokens</string>
@ -422,6 +426,7 @@
<string name="custom_token_creation_error_token_already_exist_title">O token já existe.</string>
<string name="custom_token_creation_error_wrong_decimals">O valor decimal deve ser um número inteiro válido, até um limite de %d</string>
<string name="custom_token_custom_derivation">Derivação personalizada</string>
<string name="custom_token_custom_derivation_dynamic_addresses_enabled_error">O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis.</string>
<string name="custom_token_custom_derivation_placeholder">Exemplo: m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Insira uma derivação personalizada</string>
<string name="custom_token_decimals_input_title">Decimais</string>
@ -474,6 +479,19 @@
<string name="domain_receive_assets_onboarding_description">O envio de ativos por outras redes resultará em perda permanente.</string>
<string name="domain_receive_assets_onboarding_network_name">%s rede</string>
<string name="domain_receive_assets_onboarding_title">Envie fundos usando apenas</string>
<string name="dynamic_addresses">Endereços dinâmicos</string>
<string name="dynamic_addresses_custom_token_error_on_addition">O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis.</string>
<string name="dynamic_addresses_enabled_toast_title">Endereços dinâmicos ativados</string>
<string name="dynamic_addresses_enter_features_privacy_description">Use um novo endereço para cada transação para reduzir a rastreabilidade e melhorar a privacidade na blockchain.</string>
<string name="dynamic_addresses_enter_features_privacy_title">Privacidade aprimorada</string>
<string name="dynamic_addresses_enter_features_receving_description">Receba fundos facilmente em redes baseadas em UTXO com geração automática de endereços — sem necessidade de gerenciamento manual de endereços.</string>
<string name="dynamic_addresses_enter_features_receving_title">Recebimento perfeito</string>
<string name="dynamic_addresses_enter_main_button_title">Habilitar endereços dinâmicos</string>
<string name="dynamic_addresses_enter_subtitle">Endereços dinâmicos criam um novo a cada vez para maior privacidade — seu saldo total permanece o mesmo. </string>
<string name="dynamic_addresses_error_has_custom_token_description">Não é possível ativar os endereços dinâmicos porque alguns endereços/tokens personalizados usam um caminho de derivação modificado, que não atende aos critérios necessários.</string>
<string name="dynamic_addresses_error_has_custom_token_title">Endereços dinâmicos indisponíveis</string>
<string name="dynamic_addresses_error_service_unavailable_description">Não foi possível conectar-se ao provedor neste momento. Tente novamente mais tarde.</string>
<string name="dynamic_addresses_error_service_unavailable_title">Serviço indisponível. Por favor, tente novamente.</string>
<string name="earn_best_opportunities">Melhores oportunidades</string>
<string name="earn_clear_filter">Limpar filtro</string>
<string name="earn_empty">A lista está temporariamente vazia, pois está sendo atualizada. Volte daqui a pouco.</string>
@ -570,6 +588,8 @@
<string name="fee_selector_choose_token_description">Escolha qual token usar para pagar a taxa de rede. %s</string>
<string name="fee_selector_choose_token_title">Escolha o token</string>
<string name="feed_market_and_news">Mercado e Notícias</string>
<string name="feed_search_show_all_user_assets">Mostrar tudo</string>
<string name="feed_search_show_less_user_assets">Mostrar menos</string>
<string name="feed_trending_now">Em alta agora</string>
<string name="feedback_data_collection_message">As informações a seguir são opcionais. Você pode apagá-las se não quiser compartilhá-las.</string>
<string name="feedback_preface_rate_negative">Diga-nos quais funções estão faltando e tentaremos ajudá-lo.</string>
@ -705,6 +725,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Limite de mana</string>
<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_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>
@ -750,9 +771,14 @@
<string name="markets_insights_info_description_message">Os dados desta seção são provenientes das seguintes redes: %s</string>
<string name="markets_loading_error_title">Não foi possível carregar os dados…</string>
<string name="markets_loading_no_data_title">Sem dados</string>
<string name="markets_portfolio_block_add_token_title">**Adicione ao seu portfólio** para começar a comprar, trocar ou receber este ativo.</string>
<string name="markets_portfolio_block_subtitle">Em seu portfólio</string>
<string name="markets_pulse_common_title">Pulso do mercado</string>
<string name="markets_quick_actions">Ações rápidas</string>
<string name="markets_search_clear_all_hints">Limpar tudo</string>
<string name="markets_search_header_title">Pesquisar tokens</string>
<string name="markets_search_hint_header">Recentes</string>
<string name="markets_search_portfolio_header">Em seu portfólio</string>
<string name="markets_search_result_title">Resultado</string>
<string name="markets_search_see_tokens_under_100k">Veja tokens com capitalização de mercado inferior a 100 mil dólares.</string>
<string name="markets_search_show_tokens">Mostrar tokens</string>
@ -1114,6 +1140,8 @@
<string name="qr_scanner_error_unrecognized_title">Código QR não reconhecido</string>
<string name="qr_scanner_error_unsupported_network_message">Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas.</string>
<string name="qr_scanner_error_unsupported_network_title">Nenhum token compatível encontrado</string>
<string name="qr_scanner_warning_unknown_parameters_message">Este código QR contém parâmetros que não são reconhecidos: %sAlgumas informações de pagamento podem ser perdidas se você continuar.</string>
<string name="qr_scanner_warning_unknown_parameters_title">Parâmetros desconhecidos</string>
<string name="receive_bottom_sheet_no_memo_required_message">Não é necessário memorando</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) sobre %3$s rede</string>
<string name="receive_bottom_sheet_warning_message_compact">%1$s sobre %2$s rede</string>
@ -1499,6 +1527,7 @@
<string name="swapping_fee_estimation_error_text">Erro na estimativa de custos. Por favor, envie seu feedback para o suporte.</string>
<string name="swapping_from_title">Você troca</string>
<string name="swapping_high_price_impact_description">Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado.</string>
<string name="swapping_high_price_impact_text">Você pode receber um valor significativamente menor devido à baixa liquidez. Tente um valor menor ou outro provedor.</string>
<string name="swapping_high_price_impact_title">Alto impacto nos preços</string>
<string name="swapping_insufficient_funds">Fundos insuficientes</string>
<string name="swapping_insufficient_funds_description">Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos.</string>
@ -1508,8 +1537,11 @@
<string name="swapping_to_title">Você recebe</string>
<string name="swapping_token_list_title">Escolha o token</string>
<string name="swapping_token_not_available">não disponível</string>
<string name="swapping_trade_too_large_text">Liquidez insuficiente para esta transação. \n o valor ou escolha outro provedor.</string>
<string name="swapping_trade_too_large_title">Negociação em grande escala</string>
<string name="tangem_pay_beta_notification_subtitle">Teremos todo o prazer em receber seu feedback.</string>
<string name="tangem_pay_beta_notification_title">O Tangem Pay agora está em versão beta.</string>
<string name="tangem_pay_card_details_unable_to_rename_card_title">Não foi possível renomear o cartão.</string>
<string name="tangem_pay_card_frozen">Cartão bloqueado</string>
<string name="tangem_pay_card_payment">Pagamento com cartão</string>
<string name="tangem_pay_deposit">Depósito</string>
@ -1543,7 +1575,9 @@
<string name="tangem_pay_unfreeze_card_success">Seu cartão foi desbloqueado.</string>
<string name="tangem_pay_withdrawal">Retirada</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>
<string name="tangempay_card_default_name">Cartão Tangem Pay 1</string>
<string name="tangempay_card_details_add_funds">Adicionar fundos</string>
<string name="tangempay_card_details_add_funds_subtitle">Opções de recarga</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Adicionar ao Google Wallet</string>
@ -1576,10 +1610,13 @@
<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_rename_card_invalid_description">Somente letras e números são permitidos.</string>
<string name="tangempay_card_details_rename_card_invalid_title">Caracteres inválidos</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>
<string name="tangempay_card_details_view_pin_code_description">Volte ao aplicativo se você se esquecer.</string>
<string name="tangempay_card_details_view_pin_code_title">Seu código PIN</string>
@ -1587,12 +1624,16 @@
<string name="tangempay_card_details_withdraw_error_title">Retirada indisponível agora</string>
<string name="tangempay_card_details_withdraw_in_progress_description">Você não pode iniciar uma troca ou um novo saque até que o atual seja concluído.</string>
<string name="tangempay_card_details_withdraw_in_progress_title">Retirada em andamento</string>
<string name="tangempay_card_page_settings_title">Configurações do cartão</string>
<string name="tangempay_change_pin_code">Alterar código PIN</string>
<string name="tangempay_come_back_if_forget_pin">Volte ao aplicativo se você se esquecer.</string>
<string name="tangempay_digital_card">Cartão digital</string>
<string name="tangempay_factory_settings_warning_title">Entendo que perderei completamente o acesso ao meu cartão Tangem Pay e a todos os fundos nele contidos, sem possibilidade de recuperação.</string>
<string name="tangempay_failed_to_issue_card">Falha na emissão do cartão</string>
<string name="tangempay_failed_to_issue_card_retry_description">Ocorreu um erro técnico. Tente novamente clicando no botão abaixo.</string>
<string name="tangempay_failed_to_issue_card_support_description">Ocorreu um erro técnico. Por favor, entre em contato com o suporte.</string>
<string name="tangempay_feature_will_be_available_soon">O recurso estará disponível em breve.</string>
<string name="tangempay_feature_will_be_available_soon_description">Você poderá emitir cartões adicionais para sua conta de pagamento.</string>
<string name="tangempay_get_banner_description">Obtenha seu cartão virtual Visa Tangem grátis.</string>
<string name="tangempay_get_tangem_pay">Obtenha o Tangem Pay</string>
<string name="tangempay_go_to_support">Acesse o Suporte</string>
@ -1624,6 +1665,7 @@
<string name="tangempay_onboarding_security_description">Uma conta de pagamento separada será criada sem divulgar seus endereços e bens.</string>
<string name="tangempay_onboarding_security_title">Privacidade incomparável</string>
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay gratuito em minutos.</string>
<string name="tangempay_pay_support">Suporte de pagamento</string>
<string name="tangempay_payment_account">Conta de pagamento</string>
<string name="tangempay_payment_account_sync_needed">A conta de pagamento não está sincronizada.</string>
<string name="tangempay_pin_validation_error_message">PIN inválido: evite sequências ou repetições.</string>
@ -1636,6 +1678,7 @@
<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>
<string name="tangempay_title">Tangem Pay</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">O saldo do seu Polygon em USDC na blockchain é diferente do saldo do seu cartão e é atualizado em até 2 dias úteis após uma compra. 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>
@ -1666,6 +1709,10 @@
<string name="token_details_staking_block_title">Serviço de staking</string>
<string name="token_details_token_type_subtitle">%1$s token em %%imagem%% %2$s rede</string>
<string name="token_details_token_type_subtitle_no_standard">Token em %%imagem%% %1$s rede</string>
<string name="token_details_toolbar_subtitle_network">%s rede</string>
<string name="token_details_toolbar_subtitle_standard">%1$s em %2$s rede</string>
<string name="token_details_toolbar_title_token_in_account">%1$s em %%imagem%% %2$s</string>
<string name="token_details_toolbar_title_token_in_wallet">%1$s em %2$s %%imagem%%</string>
<string name="token_details_unable_hide_alert_message">O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista</string>
<string name="token_details_unable_hide_alert_title">Não foi possível ocultar %s</string>
<string name="token_market_metrics_no_data">N/A</string>
@ -1711,6 +1758,9 @@
<string name="universal_error">Ocorreu um erro. Código do erro: %sPor favor, entre em contato com nosso suporte.</string>
<string name="unlock_wallet_description_full">Usar %s ou escaneie um cartão/anel para acessar sua carteira.</string>
<string name="unsupported_wc_version">Falha na conexão: Este aplicativo descentralizado (dApp) usa a versão 1.0 do Wallet Connect, que não é compatível. Certifique-se de que o dApp seja compatível com a versão 2.0 do Wallet Connect para que a conexão seja estabelecida com sucesso.</string>
<string name="update_approval_permission_fee_note">A permissão anterior será revogada e uma nova será emitida. A rede cobrará uma taxa de aprovação de tokens para cada uma dessas ações. Você verá uma transação de valor zero no histórico como comprovante de revogação.</string>
<string name="update_approval_permission_subtitle">A transação excede o limite de permissão concedido anteriormente.\nAtualize a permissão para prosseguir.</string>
<string name="update_approval_permission_title">Atualizar permissão</string>
<string name="upgrade_to_hardware_wallet_button_title">Faça upgrade para uma carteira de hardware.</string>
<string name="user_push_notification_agreement_argument_one">Fique por dentro das últimas novidades e recursos.</string>
<string name="user_push_notification_agreement_argument_one_subtitle">Alertas em tempo real para transações, câmbio e atualizações críticas.</string>

View file

@ -1124,6 +1124,14 @@
<string name="qr_scanner_camera_denied_settings_button">Налаштування</string>
<string name="qr_scanner_camera_denied_text">Ви не надали доступ до своєї камери</string>
<string name="qr_scanner_camera_denied_title">Доступ до камери заборонено</string>
<string name="qr_scanner_error_token_not_added_message">Запитуваний токен не додано до вашого гаманця. Будь ласка, додайте його та спробуйте ще раз.</string>
<string name="qr_scanner_error_token_not_added_title">Токен не додано</string>
<string name="qr_scanner_error_unrecognized_message">На жаль, цей QR-код не вдалося розпізнати.</string>
<string name="qr_scanner_error_unrecognized_title">Нерозпізнаний QR-код</string>
<string name="qr_scanner_error_unsupported_network_message">Ця мережа не підтримується жодним із ваших доданих токенів. Додайте підтримуваний токен, щоб надіслати криптовалюту.</string>
<string name="qr_scanner_error_unsupported_network_title">Підтримуваних токенів не знайдено</string>
<string name="qr_scanner_warning_unknown_parameters_message">Цей QR-код містить нерозпізнані параметри: %s. Деякі деталі платежу можуть бути втрачені, якщо ви продовжите.</string>
<string name="qr_scanner_warning_unknown_parameters_title">Невідомі параметри</string>
<string name="receive_bottom_sheet_no_memo_required_message">Memo не вимагається</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) у мережі %3$s</string>
<string name="receive_bottom_sheet_warning_message_compact">%1$s у мережі %2$s</string>

View file

@ -1,19 +1,650 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="common_add_tokens">添加代幣</string>
<string name="common_no_results">沒有結果</string>
<string name="common_token_send">要發送的代幣</string>
<string name="common_unlock">解鎖</string>
<string name="initial_wallet_sync_banner_description">您的錢包已同步並準備就緒。\n缺少某些代幣</string>
<string name="initial_wallet_sync_banner_title">錢包導入成功</string>
<string name="initial_wallet_sync_restore_progress">恢復%d%%</string>
<string name="access_code_alert_validation_cancel">更改代码</string>
<string name="access_code_alert_validation_description">您的访问代码可解锁和保护您的钱包访问权限</string>
<string name="access_code_alert_validation_ok">无论如何都要用</string>
<string name="access_code_alert_validation_title">这个访问码很容易猜到。</string>
<string name="account_add_limit_dialog_description">您不能创建超过 %1$s 账户。存档一个账户以添加新账户。</string>
<string name="account_add_limit_dialog_title">无法添加新帐户</string>
<string name="account_archive_success_message">账户已存档</string>
<string name="account_archived_accounts">已存档账户</string>
<string name="account_archived_recover_dialog_description">您即将恢复 \"%1$s\"。</string>
<string name="account_archived_recover_dialog_title">恢复账户</string>
<string name="account_archived_recover_error_message">您已达到20个活跃账户的上限。请归档一个账户以恢复此账户。</string>
<string name="account_archived_recover_error_title">无法恢复帐户</string>
<string name="account_could_not_archive">我们无法存档账户。请稍后再试。</string>
<string name="account_could_not_archive_referral_program_message">此账户正在参与推荐计划。</string>
<string name="account_could_not_archive_referral_program_title">此帐户无法存档</string>
<string name="account_could_not_create">我们无法创建账户。请稍后再试。</string>
<string name="account_create_success_message">账户已创建</string>
<string name="account_details_archiving">存档...</string>
<string name="account_edit_failure_dialog_title">无法编辑帐户</string>
<string name="account_edit_success_message">账户已保存</string>
<string name="account_for_rewards">奖励账户</string>
<string name="account_form_name_already_exist_error_description">已存在使用此名称的帐户。请选择其他名称。</string>
<string name="account_form_name_already_exist_error_title">帐户名已被使用</string>
<string name="account_generic_error_dialog_message">请稍后再试。如果问题仍然存在,请联系客服,我们将协助您解决问题。</string>
<string name="account_label_tokens_info">%1$s 在 %2$s</string>
<string name="account_main_account_title">主账户</string>
<string name="account_recover_limit_dialog_description">您已超过 %1$s 活动账户的限制。存档一个以恢复</string>
<string name="account_recover_limit_dialog_title">无法恢复帐户</string>
<string name="account_recover_success_message">账户已恢复</string>
<string name="account_reorder_description">长按账户可重新排序</string>
<string name="accounts_migration_alert_message">一些自定义代币将自动从 \"%1$s\" 移至 \"%2$s\",因为它们的派生词属于该账户。</string>
<string name="accounts_migration_alert_title">部分自定义代币将自动移动</string>
<string name="alert_authentication_error_message">生物识别出现问题。请尝试重置设备上的生物识别技术或联系技术支持。</string>
<string name="alert_authentication_error_title">验证错误</string>
<string name="app_settings_access_code_warning">设置访问码以启用生物识别功能</string>
<string name="app_settings_off_require_access_code_alert_message">稍后会要求您提供访问代码以进行安全存储。</string>
<string name="app_settings_on_require_access_code_alert_message">这将删除所有已保存的钱包访问码。您需要重新输入访问码才能使用钱包。</string>
<string name="biometric_disabled_warning_description">您的设备已关闭生物识别功能,因此无法使用此功能解锁钱包。请在设备设置中启用生物识别功能,即可再次使用此方法。</string>
<string name="biometric_disabled_warning_title">生物识别认证已禁用</string>
<string name="biometric_lockout_permanent_warning_description_2">您的生物识别尝试次数已达上限。请使用卡片/指环解锁钱包,或输入您的访问码。</string>
<string name="biometric_lockout_permanent_warning_title">生物识别认证已锁定</string>
<string name="biometric_lockout_warning_description_2">生物识别登录暂时锁定。请30秒后重试或轻触设备或输入访问码解锁您的钱包。</string>
<string name="biometric_updated_warning_description">您设备上的生物识别信息已更新。请选择您的钱包并输入其访问码以重新启用生物识别登录。</string>
<string name="biometric_updated_warning_title">需要注意</string>
<string name="bitcoin_promo_activation_error">处理您的优惠码时出错,请稍后再试。</string>
<string name="bitcoin_promo_activation_error_title">激活错误</string>
<string name="bitcoin_promo_activation_success">您的优惠码已成功激活。奖励将在14天内计入您的比特币账户。</string>
<string name="bitcoin_promo_activation_success_title">促销代码已激活</string>
<string name="bitcoin_promo_already_activated">此优惠码已被使用,无法再次激活。</string>
<string name="bitcoin_promo_already_activated_title">代码不可用</string>
<string name="bitcoin_promo_invalid_code">此优惠码无效,无法激活。</string>
<string name="bitcoin_promo_invalid_code_title">无效代码</string>
<string name="bitcoin_promo_no_address">您需要提供比特币地址才能领取奖励。请将比特币地址添加到您的钱包,然后重试激活。</string>
<string name="bitcoin_promo_no_address_title">需要比特币地址</string>
<string name="card_reset_alert_continue_message">请重置下一个设备以继续。</string>
<string name="card_reset_alert_continue_title">钱包重置</string>
<string name="card_reset_alert_finish_message">所有 Tangem 设备均已重置。您现在可以继续升级钱包。</string>
<string name="card_reset_alert_finish_ok_button">再次升级</string>
<string name="card_reset_alert_finish_title">重置完成</string>
<string name="card_reset_alert_incomplete_message">我们建议在此钱包中完成所有 Tangem 设备的重置过程。</string>
<string name="card_reset_alert_incomplete_title">部分 Tangem 设备仍需重置。</string>
<string name="common_account">账户</string>
<string name="common_accounts">账户</string>
<string name="common_activate">激活</string>
<string name="common_add">添加</string>
<string name="common_add_tokens">添加代币</string>
<string name="common_added">已添加</string>
<string name="common_and"></string>
<string name="common_choose_account">选择账户</string>
<string name="common_contact_support">联系客服</string>
<string name="common_estimated_fee">充值网络费</string>
<string name="common_forget">忘记</string>
<string name="common_from"></string>
<string name="common_from_wallet_name">从 %s</string>
<string name="common_get_started">开始</string>
<string name="common_get_token">获取代币</string>
<string name="common_hold_to">保持到 %s</string>
<string name="common_insufficient_balance">余额不足</string>
<string name="common_learn_more">了解更多</string>
<string name="common_legacy_bitcoin_address">传统比特币</string>
<string name="common_locked_wallets">锁定的钱包</string>
<plurals name="common_networks_count">
<item quantity="other">%d网络</item>
</plurals>
<string name="common_new_address">新地址</string>
<string name="common_news">新闻</string>
<string name="common_no_results">无结果</string>
<string name="common_not_available">不可用</string>
<string name="common_or">或者</string>
<string name="common_recommended">推荐</string>
<string name="common_reset">重置</string>
<string name="common_something_went_wrong">出问题了</string>
<string name="common_tangem">Tangem</string>
<string name="common_tap_and_hold_hint">点击并按住</string>
<string name="common_to"></string>
<string name="common_to_wallet_name">到 %s</string>
<string name="common_token_send">要发送的代币</string>
<string name="common_unable_to_load">无法加载数据……</string>
<string name="common_understand_continue">我明白,请继续</string>
<string name="common_unlock">解锁</string>
<string name="common_wallets">钱包</string>
<string name="common_yield_mode">收益模式</string>
<string name="custom_token_another_account_dialog_description">您的代币派生信息与 %1$s的派生信息一致。 您的代币将被添加到该账户。</string>
<string name="custom_token_another_account_dialog_title">派生属于另一个账户。</string>
<string name="custom_token_another_account_snackbar_text">代币已添加到 %1$s 帐户</string>
<string name="custom_token_custom_derivation_dynamic_addresses_enabled_error">动态地址已启用。自定义\“更改”\和\“索引”\不可用。</string>
<string name="details_mobile_wallet_upgrade_action_title">升级到硬件钱包</string>
<string name="details_wallets_section_description">您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。</string>
<string name="domain_receive_assets_default_address">默认地址</string>
<string name="domain_receive_assets_legacy_address">传统 %s 地址</string>
<string name="domain_receive_assets_network_name_address">%s 地址</string>
<string name="dynamic_addresses">动态地址</string>
<string name="dynamic_addresses_custom_token_error_on_addition">动态地址已启用。自定义“更改”和“索引”功能不可用。</string>
<string name="dynamic_addresses_enabled_toast_title">已启用动态地址</string>
<string name="dynamic_addresses_enter_features_privacy_description">每次交易都使用一个新地址,以减少可追溯性并提高链上隐私性。</string>
<string name="dynamic_addresses_enter_features_privacy_title">增强隐私</string>
<string name="dynamic_addresses_enter_features_receving_description">通过自动生成地址轻松在基于UTXO的网络中接收资金——无需手动管理地址。</string>
<string name="dynamic_addresses_enter_features_receving_title">无缝接收</string>
<string name="dynamic_addresses_enter_main_button_title">启用动态地址</string>
<string name="dynamic_addresses_enter_subtitle">动态地址每次都会创建一个新地址,以增强隐私保护——您的总余额保持不变。 </string>
<string name="dynamic_addresses_error_has_custom_token_description">无法启用动态地址,因为某些自定义地址/代币使用的是修改后的派生路径,不符合所需的标准。</string>
<string name="dynamic_addresses_error_has_custom_token_title">动态地址不可用</string>
<string name="dynamic_addresses_error_service_unavailable_description">我们目前无法连接到服务提供商,请稍后再试。</string>
<string name="dynamic_addresses_error_service_unavailable_title">服务不可用,请稍后再试。</string>
<string name="earn_best_opportunities">最佳机会</string>
<string name="earn_clear_filter">清除筛选</string>
<string name="earn_empty">列表正在刷新,暂时为空。请稍后再查看。</string>
<string name="earn_filter_all_networks">所有网络</string>
<string name="earn_filter_all_types">所有类型</string>
<string name="earn_filter_by">筛选方式</string>
<string name="earn_filter_my_networks">我的网络</string>
<string name="earn_filter_networks">网络</string>
<string name="earn_mostly_used">热门</string>
<string name="earn_no_results">无结果</string>
<string name="earn_title">质押与收益模式</string>
<string name="exchange_token_description">将您投资组合中的任何资产交换为该代币</string>
<string name="express_provider_great_rate">有竞争力的费率</string>
<string name="fee_selector_choose_speed_description">速度越快,确认速度越快,但网络费用也越高。 %s</string>
<string name="fee_selector_choose_speed_title">选择速度</string>
<string name="fee_selector_choose_token_description">选择用于支付网络费用的代币。 %s</string>
<string name="fee_selector_choose_token_title">选择代币</string>
<string name="feed_market_and_news">市场与新闻</string>
<string name="feed_tangem_ai">Tangem AI</string>
<string name="feed_trending_now">当下热门</string>
<string name="feedback_subject_backup_problem">备份问题</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">资金不足</string>
<string name="gasless_transaction_fee">转账费</string>
<string name="hardware_wallet_backup_feature_description">地址直接在您的 Tangem 硬件钱包上生成,随时可用,并受到全面保护。</string>
<string name="hardware_wallet_backup_feature_title">新地址</string>
<string name="hardware_wallet_create_title">添加 Tangem 钱包</string>
<string name="hardware_wallet_key_feature_description">您的私钥将直接在 Tangem 卡内生成,并且永远不会离开该卡。</string>
<string name="hardware_wallet_key_feature_title">密钥生成</string>
<string name="hardware_wallet_security_feature_description">所有加密操作都在安全芯片内部进行,该芯片经过认证,可防止克隆和物理篡改。</string>
<string name="hardware_wallet_security_feature_title">硬件级安全</string>
<string name="hot_access_code_set_biometric_ask">您是否允许“Tangem”使用生物识别认证来确认您的身份并打开应用程序</string>
<string name="hw_backup_banner_description">让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。</string>
<string name="hw_backup_hardware_create_description">创建一个安全的钱包并转移资金,以加强保护。</string>
<string name="hw_backup_hardware_create_title">创建新钱包</string>
<string name="hw_backup_hardware_description">使用卓越的 Tangem 硬件钱包,提升您的安全保障。</string>
<string name="hw_backup_hardware_title">硬件钱包</string>
<string name="hw_backup_hardware_upgrade_description">将您当前的手机钱包转换为 Tangem 冷钱包。</string>
<string name="hw_backup_hardware_upgrade_title">升级当前钱包</string>
<string name="hw_backup_need_finish_first">先完成备份</string>
<string name="hw_backup_section_other_title">其他方法</string>
<string name="hw_backup_seed_code_description">将您的恢复短语保存在安全的地方,保持其私密性以保护您的资金,并设置访问密码以提高安全性。</string>
<string name="hw_backup_to_secure_description">要使用访问码保护您的钱包,请完成备份过程。</string>
<string name="hw_backup_to_upgrade_description">要升级到硬件钱包,请完成备份过程。</string>
<string name="hw_backup_upgrade_description">将您的手机钱包升级到 Tangem 硬件钱包,享受最高级别的安全性。导入您的钱包或将资金转移到新钱包。</string>
<string name="hw_backup_upgrade_title">升级到冷钱包</string>
<string name="hw_create_upgrade_description">随时安全地将手机钱包转移到 Tangem 卡片或指环上。</string>
<string name="hw_create_upgrade_title">升级到硬件钱包</string>
<string name="hw_import_existing_wallet">导入现有钱包</string>
<string name="hw_import_seed_phrase_already_imported">此恢复短语已导入</string>
<string name="hw_remove_wallet_action_forget_title">忘记钱包</string>
<string name="hw_remove_wallet_attention_description">此钱包将从您的设备中永久移除。</string>
<string name="hw_remove_wallet_confirmation_title">你确定要这么做吗?</string>
<string name="hw_remove_wallet_nav_title">忘记钱包</string>
<string name="hw_remove_wallet_notification_action_backup_go">转到备份</string>
<string name="hw_remove_wallet_notification_action_backup_view">查看恢复短语</string>
<string name="hw_remove_wallet_notification_action_forget">忘记钱包</string>
<string name="hw_remove_wallet_notification_action_forget_anyway">确定忘记</string>
<string name="hw_remove_wallet_notification_description_has_backup">这个钱包有备份。在忘记钱包之前,请确保你能找回备份。</string>
<string name="hw_remove_wallet_notification_description_without_backup">如果您忘记了这个钱包而又没有备份,您将永远无法使用您的资金。</string>
<string name="hw_remove_wallet_notification_title">你确定要忘记这个钱包吗?</string>
<string name="hw_remove_wallet_warning_access">我知道,如果我在移除钱包之前没有备份钱包,我将无法访问钱包。</string>
<string name="hw_remove_wallet_warning_device">我知道移除钱包并不会将其删除,只会将其从我的设备中移除。</string>
<string name="hw_upgrade">升级</string>
<string name="hw_upgrade_backup_description">当您将手机钱包升级为 Tangem 卡或指环时,您的所有地址和余额都可以完全访问。</string>
<string name="hw_upgrade_backup_title">相同地址</string>
<string name="hw_upgrade_error_card_already_has_wallet">无法升级。此设备上已有一个钱包。</string>
<string name="hw_upgrade_error_card_key_import">请换一台设备。这台设备不能用于升级。</string>
<string name="hw_upgrade_error_wallet2_card_required">操作过程中发生错误。</string>
<string name="hw_upgrade_funds_access_description">在此过程中,您的资金将保持安全且完全可用。</string>
<string name="hw_upgrade_funds_access_title">获得资金</string>
<string name="hw_upgrade_general_security_description">升级后,您的移动钱包将从应用程序中移除并存储在您的 Tangem 硬件钱包中。您的助记词将保留在您身边。</string>
<string name="hw_upgrade_general_security_title">一般安全</string>
<string name="hw_upgrade_key_migration_description">私钥将从应用程序转移到您的 Tangem 硬件钱包。</string>
<string name="hw_upgrade_key_migration_title">私钥迁移</string>
<string name="hw_upgrade_scan_device">扫描设备</string>
<string name="hw_upgrade_start_action">开始升级</string>
<string name="hw_upgrade_start_description">您即将升级到我们的硬件钱包。它会将您的资产安全地存储在冷存储中。</string>
<string name="hw_upgrade_start_title">Tangem钱包</string>
<string name="hw_upgrade_title">升级到我们的硬件钱包</string>
<string name="hw_upgrade_to_cold_banner_description">使用 Tangem 的顶级硬件钱包,确保您的加密货币安全。</string>
<string name="hw_upgrade_to_cold_banner_title">将您的钱包升级到硬件安全级别</string>
<string name="initial_wallet_sync_banner_description">您的钱包已同步并准备就绪。\n缺少某些代币</string>
<string name="initial_wallet_sync_banner_title">钱包已成功导入</string>
<string name="initial_wallet_sync_restore_progress">恢复 %d%%</string>
<string name="main_qr_scan_hint">扫描二维码即可发送资金或连接到应用程序</string>
<string name="markets_about_coin_header">关于硬币</string>
<string name="markets_earn_common_title">质押与收益模式</string>
<string name="markets_hint_part_one">向上轻扫,探索市场</string>
<string name="markets_hint_part_two">寻找新的隐藏宝石</string>
<string name="markets_portfolio_block_add_token_title">**添加到您的投资组合**,即可开始购买、交换或接收此资产</string>
<string name="markets_portfolio_block_subtitle">在您的投资组合中</string>
<string name="markets_pulse_common_title">市场脉搏</string>
<string name="markets_search_clear_all_hints">全部清除</string>
<string name="markets_search_hint_header">最近的</string>
<string name="markets_search_portfolio_header">在您的投资组合中</string>
<string name="markets_search_title_placeholder">加密货币、新闻及更多</string>
<string name="markets_sort_by_yield_mode_title">收益模式</string>
<string name="markets_token_added">代币已添加</string>
<string name="markets_token_details_insight_day_timeline"></string>
<string name="markets_token_details_insight_month_timeline"></string>
<string name="markets_token_details_insight_week_timeline"></string>
<string name="markets_token_details_insight_week_timeline"></string>
<string name="markets_token_details_insight_year_timeline"></string>
<string name="send_rate_fixed_info_description">使用固定利率時,您在置換交易時收到的金額將被鎖定,這可以保護您免受交易過程中價格波動的影響。</string>
<string name="markets_token_details_max_supply_and_circulation_full">循环和最大供应</string>
<string name="markets_token_details_metrics_no_limited">无限制</string>
<string name="markets_token_details_trading_interval">24小时</string>
<string name="markets_token_details_valuation_value_in_total">%s 总共</string>
<string name="markets_tooltip_v2_title">添加更多代币</string>
<string name="markets_yield_supply_banner_description">增强资产性能,并使其能够即时访问。 %s</string>
<string name="markets_yield_supply_banner_title">激活收益模式</string>
<string name="mobile_wallet_requires_min_os_warning_body">创建手机钱包前必须更新至 %1$s</string>
<string name="mobile_wallet_requires_min_os_warning_title">手机钱包需要 %1$s 或更高版本</string>
<string name="news_all_news">所有新闻</string>
<string name="news_like">喜欢</string>
<plurals name="news_published_hours_ago">
<item quantity="other">小时之前</item>
</plurals>
<plurals name="news_published_minutes_ago">
<item quantity="other">分钟之前</item>
</plurals>
<string name="news_quick_recap">快速回顾</string>
<string name="news_related_news">新闻</string>
<string name="news_related_tokens">相关代币</string>
<string name="news_sources">相关新闻</string>
<string name="news_stay_in_the_loop">随时了解最新动态</string>
<string name="news_trending_score">趋势得分</string>
<string name="notification_black_friday_text">每个钱包均可获得价值 10 美元的 BTC \n快来领取</string>
<string name="notification_black_friday_title">黑色星期五:最多可省 30 美元% </string>
<string name="notification_one_plus_one_button">我们出发吧</string>
<string name="notification_one_plus_one_text">限时优惠!</string>
<string name="notification_one_plus_one_title">1+1买一个钱包优惠 50%</string>
<string name="notification_sepa_button">购买加密货币</string>
<string name="notification_sepa_text">通过 SEPA 转账购买加密货币时,可享受 ** 0% 费用**。</string>
<string name="notification_sepa_title">使用 SEPA 购买加密货币</string>
<string name="notification_visa_waitlist_promo_text">加入候补名单,即可获得一张与众不同的支付卡。</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa卡</string>
<string name="notification_yield_promo_button">条款和条件</string>
<string name="notification_yield_promo_text">存款 100 美元以上,持有 30 天,即可获得 10 美元</string>
<string name="notification_yield_promo_title">加入 \"收益模式 \"活动</string>
<string name="onboarding_navbar_upgrade_wallet_biometrics">生物识别</string>
<string name="only_one_mobile_wallet_explanation">您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。</string>
<string name="onramp_all_offers_button_title">所有优惠</string>
<string name="onramp_error_transaction_already_processed">此交易已处理完毕,无需进一步操作。</string>
<string name="onramp_fetching_best_rates">获得最佳利率...</string>
<string name="onramp_instant_status">即时</string>
<string name="onramp_legal_text">服务由外部供应商提供。\nTangem对此不承担任何责任。</string>
<string name="onramp_offer_type_fastet">最快处理</string>
<string name="onramp_payment_method_subtitle">付款方式</string>
<plurals name="onramp_providers_count">
<item quantity="other">提供者</item>
</plurals>
<string name="onramp_providers_subtitle">提供商</string>
<string name="onramp_recently_used_title">最近使用过</string>
<string name="onramp_recommended_title">推荐</string>
<plurals name="onramp_timing_days">
<item quantity="other">至多%d天</item>
</plurals>
<string name="onramp_timing_minutes">%s 分钟</string>
<string name="onramp_title_available_from">可从</string>
<string name="onramp_title_available_up_to">最高可提供</string>
<string name="onramp_title_you_get">你得到</string>
<string name="onramp_tos_external_providers">服务由外部供应商提供。\nTangem 不承担任何责任。</string>
<string name="onramp_up_to_rate">至多</string>
<string name="onramp_via_provider">通过 %s</string>
<string name="onramp_you_will_pay_title">您将支付</string>
<string name="provider_name_support">%s 支持</string>
<string name="qr_scanner_camera_denied_gallery_button">从图库中选择</string>
<string name="qr_scanner_camera_denied_settings_button">设置</string>
<string name="qr_scanner_camera_denied_text">您尚未授予摄像头访问权限</string>
<string name="qr_scanner_camera_denied_title">摄像头访问权限被拒绝</string>
<string name="qr_scanner_error_token_not_added_message">您请求的代币未添加到您的钱包。请添加后重试。</string>
<string name="qr_scanner_error_token_not_added_title">代币未添加</string>
<string name="qr_scanner_error_unrecognized_message">抱歉,无法识别此二维码。</string>
<string name="qr_scanner_error_unrecognized_title">无法识别的二维码</string>
<string name="qr_scanner_error_unsupported_network_message">您添加的所有代币均不支持此网络。请添加受支持的代币以发送加密货币。</string>
<string name="qr_scanner_error_unsupported_network_title">未找到支持的代币</string>
<string name="qr_scanner_warning_unknown_parameters_message">此二维码包含无法识别的参数: %s如果您继续操作部分支付信息可能会丢失。</string>
<string name="qr_scanner_warning_unknown_parameters_title">未知参数</string>
<string name="referral_address_for_rewards">奖励地址</string>
<string name="reset_card_to_factory_condition_3">我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回</string>
<string name="reset_cards_dialog_complete_description">所有 Tangem 设备均已重置。</string>
<string name="reset_cards_dialog_first_description">激活过程中出了问题。请逐一重置卡片。</string>
<string name="reset_cards_dialog_first_title">卡片验证失败</string>
<string name="reset_cards_dialog_next_device_description">请重置下一个设备以继续</string>
<string name="root_detected_warning_description">拥有 root 权限的设备安全性较低。您的数据可能面临额外的风险。</string>
<string name="root_detected_warning_title">检测到 root 访问</string>
<string name="send_from_title">您发送</string>
<plurals name="send_network_selection_hidden_tokens">
<item quantity="other">此地址与代币不兼容</item>
</plurals>
<string name="send_rate_fixed_info_description">使用固定利率时,您在互换交易时收到的金额将被锁定。这可以保护您免受交易过程中价格波动的影响。</string>
<string name="send_rate_fixed_info_title">固定利率</string>
<string name="send_rate_floating_info_description">浮動利率意味著您最終收到的金額可能會根據您開始和完成置換之間的市場情況略有變化。</string>
<string name="send_rate_floating_info_title">浮動利率</string>
<string name="send_rate_floating_info_description">动利率意味着您最终收到的金额可能会根据您开始和完成互换之间的市场情况略有变化。</string>
<string name="send_rate_floating_info_title">利率</string>
<string name="send_rate_is_fixed">利率固定</string>
<string name="send_with_swap_something_went_wrong">出错了。请再试一次。</string>
<string name="settings_upgrade_banner_advantage_feature_title">简单易用</string>
<string name="settings_upgrade_banner_description">让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">无助记词</string>
<string name="settings_upgrade_banner_superiority_feature_title">一流的硬件钱包</string>
<string name="settings_upgrade_banner_title">Tangem 冷钱包</string>
<string name="staking_account_initialization_footer">网络费用是处理和确认您在区块链上的交易时所需的小额费用。</string>
<string name="staking_account_initialization_message">要开始质押,您必须通过交易充值 1 TON 来激活您的 TON 账户。资金会保留在您的账户中,因为此步骤仅用于激活账户以进行质押。</string>
<string name="staking_account_initialization_title">激活账户</string>
<string name="staking_alert_network_fee_updated_message">网络费用已更改。请在继续操作前查看新金额。</string>
<string name="staking_alert_network_fee_updated_title">网络费用已更新</string>
<string name="staking_apr_earn_badge">年利率 %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">您的质押奖励将在 5 个周期(约 25 天)后开始发放,在此期间您的委托将由网络注册和统计。</string>
<string name="staking_details_annual_percentage_yield">年收益率</string>
<string name="staking_details_annual_percentage_yield_info">年利率 (APY):显示您一年内通过复利计算可获得的总利息。复利是指您获得的利息会添加到您的本金中,因此您还可以获得该利息的利息。</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_autocompound_rewards_earned">奖励将累积到您的质押余额中。已赚取资金: %s</string>
<string name="staking_max_amount_requirement_error">最高金额: %s</string>
<string name="staking_notification_ton_account_initialization_message">要开始质押,您需要先激活您的 TON 账户。</string>
<string name="staking_notification_ton_account_initialization_title">激活账户</string>
<string name="staking_solana_details_auto_claiming_rewards_daily_text">Solana 的奖励会自动添加到您的质押余额中,无法单独显示。</string>
<string name="swap_approve_description">批准后,您即允许智能合约在未来的交易中使用您的代币。</string>
<string name="swap_search_suggestion_hint">还在寻找其他代币?\n尝试搜索或探索其他加密货币</string>
<string name="swap_search_tooltip_description">搜索任何代币,即使它还不在你的列表中。</string>
<string name="swap_search_tooltip_title">使用搜索查找所需内容</string>
<string name="swap_your_assets_title">您的资产</string>
<string name="swapping_high_price_impact_text">由于流动性低,您收到的资金可能会大大减少。请尝试较小的金额或另一个提供商。</string>
<string name="swapping_insufficient_funds_description">账户余额不足,无法完成此交易。请减少收款金额或增加余额。</string>
<string name="swapping_swap_action_in_progress">互换...</string>
<string name="swapping_trade_too_large_text">此交易流动性不足,请减少金额或选择其他供应商。</string>
<string name="swapping_trade_too_large_title">交易额过大</string>
<string name="tangem_pay_beta_notification_subtitle">我们非常乐意收到您的反馈。</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay 现已进入测试阶段</string>
<string name="tangem_pay_card_frozen">卡片已冻结</string>
<string name="tangem_pay_card_payment">卡片支付</string>
<string name="tangem_pay_deposit">存款</string>
<string name="tangem_pay_dispute">争议</string>
<string name="tangem_pay_explore_transaction">探索交易</string>
<string name="tangem_pay_fee_subtitle">服务费</string>
<string name="tangem_pay_fee_title">费用</string>
<string name="tangem_pay_freeze_card_alert_body">确保您的资金安全。您可以随时解冻。</string>
<string name="tangem_pay_freeze_card_alert_title">冻结您的卡片?</string>
<string name="tangem_pay_freeze_card_failed">冻结卡片失败,请稍后再试。</string>
<string name="tangem_pay_freeze_card_freeze">冻结</string>
<string name="tangem_pay_freeze_card_success">您的卡片已被冻结。</string>
<string name="tangem_pay_get_help">获取帮助</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">原因: %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">其他</string>
<string name="tangem_pay_rooted_device_subtitle">无法在已 root 的设备上使用</string>
<string name="tangem_pay_status_completed">已完成</string>
<string name="tangem_pay_status_declined">拒绝</string>
<string name="tangem_pay_status_pending">待定</string>
<string name="tangem_pay_status_reversed">已反转</string>
<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_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_unable_to_use_rooted">无法在已root的设备上使用</string>
<string name="tangempay_cancel_kyc">从主屏幕隐藏 KYC 页面</string>
<string name="tangempay_card_details_add_funds">增加资金</string>
<string name="tangempay_card_details_add_funds_subtitle">充值选项</string>
<string name="tangempay_card_details_add_to_wallet_button_text">添加到 Google 钱包</string>
<string name="tangempay_card_details_card_number">卡号</string>
<string name="tangempay_card_details_change_pin">更改PIN码</string>
<string name="tangempay_card_details_change_pin_success_description">该卡已完全准备好用于支付。</string>
<string name="tangempay_card_details_change_pin_success_title">已创建 PIN 码</string>
<string name="tangempay_card_details_cvc">CVC</string>
<string name="tangempay_card_details_error_text">数据加载失败,请稍后再试。</string>
<string name="tangempay_card_details_expiry">到期</string>
<string name="tangempay_card_details_freeze_card">冻结卡片</string>
<string name="tangempay_card_details_hide_details">隐藏详情</string>
<string name="tangempay_card_details_hide_text">隐藏</string>
<string name="tangempay_card_details_open_wallet_button">打开 Google 钱包</string>
<string name="tangempay_card_details_open_wallet_notification_subtitle">只需轻点几下即可设置 Tangem Pay然后即可开始使用 Google Pay 付款。</string>
<string name="tangempay_card_details_open_wallet_notification_subtitle_apple">只需轻点几下即可设置 Tangem Pay然后即可开始使用 Apple Pay 付款。</string>
<string name="tangempay_card_details_open_wallet_notification_title">将您的卡片添加到 Google Pay</string>
<string name="tangempay_card_details_open_wallet_notification_title_apple">将您的卡片添加到 Apple Pay</string>
<string name="tangempay_card_details_open_wallet_step_1">打开 Google 钱包</string>
<string name="tangempay_card_details_open_wallet_step_1_5_apple">点击右上角的“+”按钮</string>
<string name="tangempay_card_details_open_wallet_step_1_apple">打开 Apple 钱包</string>
<string name="tangempay_card_details_open_wallet_step_2">点击“添加卡片”</string>
<string name="tangempay_card_details_open_wallet_step_2_apple">点击“借记卡或信用卡”</string>
<string name="tangempay_card_details_open_wallet_step_3">手动输入卡片信息</string>
<string name="tangempay_card_details_open_wallet_step_4">使用发送到您设备上的 OTP 来验证卡片。</string>
<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_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_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_view_pin_code_description">如果忘记了,请返回应用查看。</string>
<string name="tangempay_card_details_view_pin_code_title">您的PIN码</string>
<string name="tangempay_card_details_withdraw">提款</string>
<string name="tangempay_card_details_withdraw_error_title">目前无法提款</string>
<string name="tangempay_card_details_withdraw_in_progress_description">当前提款交易完成之前,您无法发起新的提款交易或互换交易。</string>
<string name="tangempay_card_details_withdraw_in_progress_title">提款进行中</string>
<string name="tangempay_card_page_settings_title">卡片设置</string>
<string name="tangempay_change_pin_code">更改PIN码</string>
<string name="tangempay_come_back_if_forget_pin">如果忘记了,请返回应用程序。</string>
<string name="tangempay_factory_settings_warning_title">我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回</string>
<string name="tangempay_failed_to_issue_card">发卡失败</string>
<string name="tangempay_failed_to_issue_card_retry_description">发生技术故障,请点击下方按钮重试。</string>
<string name="tangempay_failed_to_issue_card_support_description">发生技术故障,请联系技术支持。</string>
<string name="tangempay_get_banner_description">免费领取您的 Tangem Visa 虚拟卡</string>
<string name="tangempay_get_tangem_pay">获取 Tangem Pay</string>
<string name="tangempay_go_to_support">前往支持页面</string>
<string name="tangempay_issue_card_notification_description">通常需要最多15分钟</string>
<string name="tangempay_issue_card_notification_title">设置您的 Tangem 卡</string>
<string name="tangempay_issuing_your_card">正在发放您的卡片</string>
<string name="tangempay_issuing_your_card_description">该卡通常会在 5 分钟内自动发放。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
<string name="tangempay_kyc_confirm_cancellation_alert_title">确认取消</string>
<string name="tangempay_kyc_confirm_cancellation_description">您确定要停止 KYC 流程吗?您可以随时返回继续。</string>
<string name="tangempay_kyc_failed_description">我们无法验证您的个人资料。如有任何疑问,请联系客服。</string>
<string name="tangempay_kyc_failed_title">很抱歉,我们无法验证您的身份。 </string>
<string name="tangempay_kyc_has_failed">KYC 被拒</string>
<string name="tangempay_kyc_in_progress">KYC正在进行中</string>
<string name="tangempay_kyc_in_progress_notification_button">查看状态</string>
<string name="tangempay_kyc_in_progress_notification_title">Tangem Pay 的 KYC 流程正在进行中</string>
<string name="tangempay_kyc_in_progress_popup_description">文件通常会在 5 分钟内自动验证。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。</string>
<string name="tangempay_kyc_rejected">已拒绝</string>
<string name="tangempay_kyc_rejected_button_text">隐藏 KYC 块</string>
<string name="tangempay_kyc_rejected_description">抱歉,我们无法验证</string>
<string name="tangempay_kyc_rejected_description_span">您的个人资料。</string>
<string name="tangempay_onboarding_banner_description">免费领取您的 Tangem Visa 虚拟卡</string>
<string name="tangempay_onboarding_banner_title">使用 USDC 进行日常支付</string>
<string name="tangempay_onboarding_get_card_button_text">获取卡片</string>
<string name="tangempay_onboarding_pay_description">数字卡可与 Apple Pay 和 Google Pay 一起使用</string>
<string name="tangempay_onboarding_pay_title">随意支配您的资产</string>
<string name="tangempay_onboarding_purchases_description">购买无需支付任何额外费用</string>
<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_payment_account">支付账户</string>
<string name="tangempay_payment_account_sync_needed">支付账户未同步</string>
<string name="tangempay_pin_validation_error_message">无效PIN码请避免使用连续或重复的密码。</string>
<string name="tangempay_service_unavailable_description">我们正在修复技术问题,请稍后再试。</string>
<string name="tangempay_service_unavailable_title">服务暂时不可用</string>
<string name="tangempay_service_unreachable_try_later">无法显示详细信息。但刷卡支付功能仍然可用。</string>
<string name="tangempay_set_pin_code">设置 PIN 码</string>
<string name="tangempay_sync_needed">会话已过期</string>
<string name="tangempay_sync_needed_restore_access">恢复访问权限</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_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>
<string name="token_button_unavailability_reason_yield_supply_approval">授权已被撤销。您的资金仍处于收益模式。如需执行操作,请进入收益模式并重新授予权限。</string>
<string name="token_market_metrics_no_data">不适用</string>
<string name="token_receive_show_qr_code_title">显示二维码</string>
<string name="transaction_history_transaction_for_address">为 %s</string>
<string name="update_approval_permission_fee_note">之前的授权将被撤销,并颁发新的授权。网络将对每次此类操作收取代币批准费。您将在交易记录中看到一笔金额为零的交易,作为撤销授权的证据。</string>
<string name="update_approval_permission_subtitle">交易金额超过先前授权的金额。\n请更新授权以继续</string>
<string name="update_approval_permission_title">更新权限</string>
<string name="upgrade_to_hardware_wallet_button_title">升级到硬件钱包</string>
<string name="user_push_notification_agreement_argument_one_subtitle">实时提醒交易、兑换和重要更新。</string>
<string name="user_push_notification_agreement_argument_one_title">交易提醒</string>
<string name="user_push_notification_agreement_argument_two_subtitle">抢先体验最新功能和专属优惠。</string>
<string name="user_push_notification_agreement_argument_two_title">专题报道和新闻更新</string>
<string name="user_push_notification_banner_subtitle">启用推送通知,即可在资金到账时收到提醒。</string>
<string name="user_push_notification_banner_title">不要错过任何一笔交易</string>
<string name="user_wallet_list_delete_hw_prompt">如果您在没有备份的情况下删除此钱包,您将永久失去对您的资金的访问权限。</string>
<string name="wallet_add_common_title">选择您的钱包类型</string>
<string name="wallet_add_hardware_description">扫描您的 Tangem 卡或指环以恢复卡号或从其他钱包导入卡号。</string>
<string name="wallet_add_hardware_info_create">创建硬件钱包</string>
<string name="wallet_add_hardware_purchase">想购买 Tangem 钱包吗?</string>
<string name="wallet_add_import_seed_phrase">导入助记词</string>
<string name="wallet_add_mobile_description">在手机上恢复您的钱包或从其他应用程序导入—方便,但安全性不如 Tangem 卡。</string>
<string name="wallet_add_support_title">选择哪个?</string>
<string name="wallet_connect_multiple_transactions">多部分交易</string>
<string name="wallet_connect_multiple_transactions_description">为了顺利完成交易,您的交易将被拆分成多个部分。您需要多次刷卡才能完成交易。</string>
<string name="wallet_connect_sending_multiple_explanation">交易正在处理中。请多次用卡轻触以完成交易。</string>
<string name="wallet_connect_sending_multiple_tx">交易进行中</string>
<string name="wallet_create_common_title">创建 Tangem 钱包</string>
<string name="warning_clore_migration_button">开始迁移</string>
<string name="warning_clore_migration_copy_button">复制</string>
<string name="warning_clore_migration_description">要继续使用您的资金,请根据 Clore 官方指南开始迁移。</string>
<string name="warning_clore_migration_error_signing_not_supported">该网络不支持信息签名</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">无法签署信息。请重试。</string>
<string name="warning_clore_migration_message">根据 Clore 的官方文档,所有在 12 月 21 日之前收到的 Clore 代币ERC-20 代币)都将迁移到 Clore此日期之后收到的代币则不会迁移。迁移方案即将推出敬请期待。</string>
<string name="warning_clore_migration_message_label">消息</string>
<string name="warning_clore_migration_open_portal_button">打开索赔通道</string>
<string name="warning_clore_migration_sheet_description">要继续使用您的 Clore 代币,您必须根据索赔通道上的信息完成代币迁移。</string>
<string name="warning_clore_migration_sheet_title">Clore 网络迁移</string>
<string name="warning_clore_migration_sign_button">签署</string>
<string name="warning_clore_migration_signature_label">签名</string>
<string name="warning_clore_migration_title">Clore 网络迁移</string>
<string name="warning_express_unsupported_pair_description">此交易对不支持互换。请选择其他代币重试。</string>
<string name="warning_express_unsupported_pair_title">不支持的互换对</string>
<string name="wc_alert_add_network_to_portfolio_description">所需区块链 %s 尚未添加到您的投资组合中。请先添加,然后再进行连接。</string>
<string name="wc_alert_add_network_to_portfolio_title">将区块链添加到投资组合</string>
<string name="wc_alert_network_not_connected_description">要继续,请将您的 dApp 会话重新连接到所需的区块链网络 %s。</string>
<string name="wc_alert_network_not_connected_title">区块链网络未连接</string>
<string name="wc_alert_request_timeout_description">检查您的区块链连接</string>
<string name="wc_alert_request_timeout_title">请求超时</string>
<string name="wc_alert_unsupported_network_description">该区块链 %s Tangem Wallet 不支持,无法连接。</string>
<string name="wc_alert_unsupported_network_title">不支持的区块链</string>
<string name="wc_approve_description">通过批准,您允许 dApp 或智能合约在未来的交易中使用代币。</string>
<string name="welcome_create_wallet_already_have">已经拥有 Tangem 钱包?</string>
<string name="welcome_create_wallet_feature_assets">数千种资产</string>
<string name="welcome_create_wallet_feature_class">顶级硬件钱包</string>
<string name="welcome_create_wallet_feature_delivery">快速发货</string>
<string name="welcome_create_wallet_feature_one_tap">一触即开始</string>
<string name="welcome_create_wallet_feature_seamless">无缝且安全</string>
<string name="welcome_create_wallet_feature_seedphrase">无助记词</string>
<string name="welcome_create_wallet_feature_use">简单易用</string>
<string name="welcome_create_wallet_hardware_description">用 Tangem 创建硬件钱包。像银行卡一样纤薄,像银行保险库一样安全。</string>
<string name="welcome_create_wallet_mobile_description">创建或导入软件钱包</string>
<string name="welcome_create_wallet_mobile_description_full">在您的手机上创建或导入软件钱包。</string>
<string name="welcome_create_wallet_mobile_title">从手机钱包开始</string>
<string name="welcome_create_wallet_other_method">其他方法</string>
<string name="welcome_create_wallet_use_hardware_description">使用 Tangem 硬件钱包</string>
<string name="welcome_create_wallet_use_hardware_title">了解更多并购买</string>
<string name="yield_module_alert_description">启用收益模式后,所有未来充值到此地址的资金都将转入 Aave。您仍然可以自由管理您的资金。</string>
<string name="yield_module_alert_title">你的 %s 提供给 Aave</string>
<string name="yield_module_amount_not_transfered_to_aave_title">供应 %1$s %2$s 到 Aave</string>
<string name="yield_module_approve_needed_notification_cta">批准</string>
<string name="yield_module_approve_needed_notification_description">您的代币授权已被撤销。请重新授权代币以恢复服务功能。</string>
<string name="yield_module_approve_needed_notification_title">需要批准</string>
<string name="yield_module_approve_sheet_fee_note">费用将被扣除,您的资产将被重新提供。</string>
<string name="yield_module_approve_sheet_subtitle">要继续产生收益,需要获得批准。</string>
<string name="yield_module_approve_sheet_title">确认批准</string>
<string name="yield_module_balance_info_sheet_subtitle">您的资金目前已提供给 Aave 协议,但您可以随时对其进行管理。</string>
<string name="yield_module_balance_info_sheet_title">你的 %s 提供给 Aave</string>
<string name="yield_module_chart_loading_error">无法加载图表...</string>
<string name="yield_module_deposit_error_notification_title">供应 %1$s %2$s 到 Aave。</string>
<string name="yield_module_disable_button">禁用收益模式</string>
<string name="yield_module_earn_badge">APY %1$s%%</string>
<string name="yield_module_earn_sheet_available_title">可用</string>
<string name="yield_module_earn_sheet_current_apy_title">当前年收益率</string>
<string name="yield_module_earn_sheet_fee_description">充值贷款时,将从余额中扣除不超过 %1$s 的网络费。</string>
<string name="yield_module_earn_sheet_high_fee_description">目前网络费用过高,无法执行借贷。一旦降至 %1$s 或以下,将立即提供资金。</string>
<string name="yield_module_earn_sheet_my_funds_title">我的资金</string>
<string name="yield_module_earn_sheet_provider_description">您的 %1$s 现已部署到 Aave 并产生收益。您持有 %2$s 这些代币代表您的余额,并会自动累积收益。当您充值时,扣除手续费后,资金将提供给 Aave 以产生更多收益。</string>
<string name="yield_module_earn_sheet_title">收益模式</string>
<string name="yield_module_earn_sheet_total_earnings_title">总收益</string>
<string name="yield_module_earn_sheet_transfers_title">转至Aave</string>
<string name="yield_module_explore_sheet_explore_aave_button_title">探索 Aave</string>
<string name="yield_module_fee_policy_sheet_current_fee_note">这是 %s上的当前供应费。实际费用将显示在激活选项卡上。</string>
<string name="yield_module_fee_policy_sheet_current_fee_title">当前费用</string>
<string name="yield_module_fee_policy_sheet_description">在扣除交易费后,今后所有 %s 充值都将自动提供给 Aave。</string>
<string name="yield_module_fee_policy_sheet_fee_note">以后每次充值都会扣除大约 %1$s (%2$s) 的网络费,但不会超过 %3$s (%4$s) 的限额。</string>
<string name="yield_module_fee_policy_sheet_max_fee_note">如果网络费用超过最高限额,交易将无法完成,直到网络费用降低为止。您可以稍后更改此限额。</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">最高费用</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">最低金额根据当前网络费用计算,确保不超过充值金额的 4%% ,相当于最低 %1$s (%2$s)。</string>
<string name="yield_module_fee_policy_sheet_min_amount_title">最低充值金额</string>
<string name="yield_module_fee_policy_sheet_title">充值费用政策</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem 还从产生的收益中提取 15% 服务费。</string>
<string name="yield_module_high_fee_error">一旦网络费用降低或您的余额达到最低金额要求,您的资金将自动转入 Aave。</string>
<string name="yield_module_high_network_fees_notification_description">由于市场活跃,手续费高于平时。您可以立即进行交易,也可以稍后手续费降低时再来查看。</string>
<string name="yield_module_high_network_fees_notification_title">高昂的网络费用</string>
<string name="yield_module_historical_returns">历史回报</string>
<string name="yield_module_main_screen_promo_banner_message">启动 %1$s%% 您余额的APY</string>
<string name="yield_module_main_view_approve_notification_description">您的代币在收益模式下的授权已被撤销。请打开代币以重新授予权限。</string>
<string name="yield_module_main_view_approve_notification_title">需要代币批准</string>
<string name="yield_module_network_fee_unreachable_notification_description">检查您的网络连接</string>
<string name="yield_module_network_fee_unreachable_notification_title">网络费用信息无法获取</string>
<string name="yield_module_promo_screen_auto_balance_subtitle">每次充值都将自动提供给 Aave。</string>
<string name="yield_module_promo_screen_auto_balance_subtitle_v2">您账户上的所有 %1$s 都将自动供应给 Aave。</string>
<string name="yield_module_promo_screen_auto_balance_title">自动供应至Aave</string>
<string name="yield_module_promo_screen_cash_out_subtitle">您可以随时随地发送、兑换或出售您的资金。</string>
<string name="yield_module_promo_screen_cash_out_title">无锁仓</string>
<string name="yield_module_promo_screen_how_it_works_button_title">它是如何运作的?</string>
<string name="yield_module_promo_screen_self_custodial_subtitle">Aave 是一个链上协议,提供非托管流动性市场,使用户能够以可变利率累积收益。</string>
<string name="yield_module_promo_screen_self_custodial_title">去中心化和自托管</string>
<string name="yield_module_promo_screen_terms_disclaimer">使用此服务即表示您同意服务提供商的条款\n%1$s 和 %2$s</string>
<string name="yield_module_promo_screen_title">连接到 Aave</string>
<string name="yield_module_promo_screen_title_v2">启动 %1$s%% APY\非您的余额</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave %1$s%% - 可变利率</string>
<string name="yield_module_promo_screen_variable_rate_info_v2">浮动利率</string>
<string name="yield_module_provider">Aave</string>
<string name="yield_module_rate_info_sheet_chart_average">平均值 %s</string>
<string name="yield_module_rate_info_sheet_chart_title">去年的收益</string>
<string name="yield_module_rate_info_sheet_description">当前利率始终是浮动的,由 Aave 的链上智能合约根据实时供求关系自动计算。</string>
<string name="yield_module_rate_info_sheet_powered_by">技术支持</string>
<string name="yield_module_rate_info_sheet_title">利率为浮动利率</string>
<string name="yield_module_receive_sheet_description">充值后,您的资金将自动转入 Aave 账户,开始产生收益。 %s 将从中扣除交易手续费。</string>
<string name="yield_module_start_earning">供应资产</string>
<string name="yield_module_start_earning_sheet_description">您的 %s 将提供给 Aave ,没有锁仓,并且可以完全访问。</string>
<string name="yield_module_start_earning_sheet_fee_policy">请参见充值费用政策</string>
<string name="yield_module_start_earning_sheet_next_deposits">您的下一笔充值将自动发送至 Aave。</string>
<string name="yield_module_start_earning_sheet_next_deposits_v2">您未来所有的收入 %1$s 存款将自动存入 Aave。</string>
<string name="yield_module_status_active">活跃</string>
<string name="yield_module_status_paused">暂停</string>
<string name="yield_module_stop_earning">禁用收益模式</string>
<string name="yield_module_stop_earning_sheet_description">关闭此功能会将您的资产从 Aave 提取,并将其转换回 %s 放入钱包,同时收益累积停止。</string>
<string name="yield_module_stop_earning_sheet_fee_note">退出收益模式时,区块链会收取网络费用。</string>
<string name="yield_module_stop_earning_sheet_title">禁用收益模式</string>
<string name="yield_module_supply">供应</string>
<string name="yield_module_supply_apr">供应 APY</string>
<string name="yield_module_token_details_earn_notification_apy">APY</string>
<string name="yield_module_token_details_earn_notification_description">利息自动累积。</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_subtitle">利息自动累积</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">收益模式</string>
<string name="yield_module_token_details_earn_notification_processing">启用收益模式</string>
<string name="yield_module_token_details_earn_notification_title">收益模式</string>
<string name="yield_module_transaction_deploy_contract">收益模式合约部署</string>
<string name="yield_module_transaction_enter">已启用收益模式</string>
<string name="yield_module_transaction_enter_subtitle">%1$s 供应给 Aave</string>
<string name="yield_module_transaction_exit">已禁用收益模式</string>
<string name="yield_module_transaction_exit_subtitle">%1$s 已从 Aave 提款</string>
<string name="yield_module_transaction_initialize">收益模式已初始化</string>
<string name="yield_module_transaction_reactivate">收益模式重新激活</string>
<string name="yield_module_transaction_topup">供应给 Aave</string>
<string name="yield_module_transaction_topup_subtitle">%1$s 供应给 Aave</string>
<string name="yield_module_transaction_withdraw">从 Aave提款</string>
<string name="yield_module_transfer_mode_automatic">自动</string>
<string name="yield_module_unable_to_cover_fee_description">添加 %1$s %2$s 以支付交易网络费。</string>
<string name="yield_module_unable_to_cover_fee_title">无法覆盖 %s 费用</string>
<string name="yield_module_unavailable_subtitle">收益模式暂时不可用。请稍后再试。</string>
<string name="yield_module_unavailable_title">收益模式不可用</string>
<string name="yield_supply_chart_loading_error">无法加载图表...</string>
</resources>

View file

@ -80,6 +80,10 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Add tokens</string>
<string name="action_buttons_you_want_to_receive">Choose the token you want to receive</string>
<string name="action_buttons_you_want_to_swap">Choose the token you want to swap</string>
<string name="add_and_manage_sheet_manage_subtitle">Add to your portfolio</string>
<string name="add_and_manage_sheet_manage_title">Add tokens</string>
<string name="add_and_manage_sheet_organize_subtitle">Sorting and grouping</string>
<string name="add_and_manage_sheet_organize_title">Organize tokens</string>
<string name="add_custom_token_choose_network">Choose network</string>
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
@ -477,7 +481,13 @@
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
<string name="dynamic_addresses">Dynamic addresses</string>
<string name="dynamic_addresses_custom_token_error_on_addition">Dynamic Addresses is enabled. Custom \"change\" and \"index\" are unavailable.</string>
<string name="dynamic_addresses_disable_description">You are turning off Dynamic Addresses. Funds will now be received on your fixed address.</string>
<string name="dynamic_addresses_disable_fee_description">A network fee applies to consolidate funds into the fixed address.</string>
<string name="dynamic_addresses_disable_main_button_title">Disable Dynamic Addresses</string>
<string name="dynamic_addresses_disable_title">Disable dynamic addresses</string>
<string name="dynamic_addresses_disabled_popup_title">Dynamic addresses disabled</string>
<string name="dynamic_addresses_enabled_toast_title">Dynamic addresses enabled</string>
<string name="dynamic_addresses_receive_badge">Dynamic address</string>
<string name="dynamic_addresses_enter_features_privacy_description">Use a new address for each transaction to reduce traceability and improve on-chain privacy.</string>
<string name="dynamic_addresses_enter_features_privacy_title">Enhanced Privacy</string>
<string name="dynamic_addresses_enter_features_receving_description">Easily receive funds in UTXO-based networks with automatic address generation — no manual address management required.</string>
@ -584,6 +594,8 @@
<string name="fee_selector_choose_token_description">Choose which token to use to pay\nthe network fee. %s</string>
<string name="fee_selector_choose_token_title">Choose token</string>
<string name="feed_market_and_news">Market &amp; News</string>
<string name="feed_search_show_all_user_assets">Show All</string>
<string name="feed_search_show_less_user_assets">Show less</string>
<string name="feed_tangem_ai">Tangem AI</string>
<string name="feed_trending_now">Trending Now</string>
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
@ -720,6 +732,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Mana limit</string>
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Mana level</string>
<string name="main_add_and_manage_tokens">Add &amp; Manage</string>
<string name="main_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>
@ -1535,6 +1548,7 @@
<string name="swapping_trade_too_large_title">Trade too large</string>
<string name="tangem_pay_beta_notification_subtitle">We would be happy to receive your feedback</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay is now in beta</string>
<string name="tangem_pay_card_details_unable_to_rename_card_title">Unable to rename card</string>
<string name="tangem_pay_card_frozen">Card frozen</string>
<string name="tangem_pay_card_payment">Card payment</string>
<string name="tangem_pay_deposit">Deposit</string>
@ -1568,7 +1582,9 @@
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
<string name="tangem_pay_withdrawal">Withdrawal</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>
<string name="tangempay_card_default_name">Tangem Pay Card 1</string>
<string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Add to Google Wallet</string>
@ -1601,10 +1617,13 @@
<string name="tangempay_card_details_receive_description">Share your address or show QR-code</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
<string name="tangempay_card_details_rename_card_invalid_description">Only letters and numbers are allowed.</string>
<string name="tangempay_card_details_rename_card_invalid_title">Invalid characters</string>
<string name="tangempay_card_details_reveal_text">Reveal</string>
<string name="tangempay_card_details_show_details">Show details</string>
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Please try again later.</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
<string name="tangempay_card_details_view_pin_code_description">Come back to the app if you forget it.</string>
<string name="tangempay_card_details_view_pin_code_title">Your PIN code</string>
@ -1612,12 +1631,16 @@
<string name="tangempay_card_details_withdraw_error_title">Withdraw unavailable now</string>
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
<string name="tangempay_card_details_withdraw_in_progress_title">Withdrawal in progress</string>
<string name="tangempay_card_page_settings_title">Card settings</string>
<string name="tangempay_change_pin_code">Change PIN-code</string>
<string name="tangempay_come_back_if_forget_pin">Come back to the app if you forget it.</string>
<string name="tangempay_digital_card">Digital card</string>
<string name="tangempay_factory_settings_warning_title">I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery</string>
<string name="tangempay_failed_to_issue_card">Failed to issue card</string>
<string name="tangempay_failed_to_issue_card_retry_description">A technical error has occurred, please try again by clicking the button below.</string>
<string name="tangempay_failed_to_issue_card_support_description">A technical error has occurred, please contact support.</string>
<string name="tangempay_feature_will_be_available_soon">Feature will be availible soon</string>
<string name="tangempay_feature_will_be_available_soon_description">You will be available to issue additional cards for your payment account</string>
<string name="tangempay_get_banner_description">Get your free Tangem Visa virtual card</string>
<string name="tangempay_get_tangem_pay">Get Tangem Pay</string>
<string name="tangempay_go_to_support">Go to Support</string>
@ -1649,6 +1672,7 @@
<string name="tangempay_onboarding_security_description">A separate payment account will be created without disclosing your addresses and assets</string>
<string name="tangempay_onboarding_security_title">Unrivaled privacy</string>
<string name="tangempay_onboarding_title">Get your free 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 is not synced</string>
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
@ -1661,6 +1685,7 @@
<string name="tangempay_tangem_visa_card">Use USDC for everyday payments</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unreachable</string>
<string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_usdc_on_polygon_network">USDC on Polygon network</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Click the button below to restore access</string>
<string name="tangempay_withdrawal_note_description">Your on-chain USDC Polygon balance differs from your card balance and updates within 2 business days after a purchase. Funds from refunded purchases wont be returned your on-chain balance or be available for withdrawal, but will stay on your card balance for purchases.</string>
<string name="tangempay_withdrawal_note_title">Please note</string>
@ -1691,6 +1716,10 @@
<string name="token_details_staking_block_title">Staking Service</string>
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
<string name="token_details_toolbar_subtitle_network">%s network</string>
<string name="token_details_toolbar_subtitle_standard">%1$s in %2$s network</string>
<string name="token_details_toolbar_title_token_in_account">%1$s in %%image%% %2$s</string>
<string name="token_details_toolbar_title_token_in_wallet">%1$s in %2$s %%image%%</string>
<string name="token_details_unable_hide_alert_message">The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_market_metrics_no_data">N/A</string>

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -61,6 +62,7 @@ internal fun ContentIcon(
background = icon.background,
alpha = alpha,
)
is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(modifier = modifier, size = icon.size)
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
modifier = modifier,
resId = icon.resId,

View file

@ -62,6 +62,7 @@ fun CurrencyIcon(
is CurrencyIconState.FiatIcon,
is CurrencyIconState.CustomTokenIcon,
is CurrencyIconState.TokenIcon,
is CurrencyIconState.PaymentAccount,
is CurrencyIconState.CryptoPortfolio.Icon,
is CurrencyIconState.CryptoPortfolio.Letter,
-> {

View file

@ -88,6 +88,12 @@ sealed class CurrencyIconState {
override val topBadgeIconResId: Int? = null
}
data class PaymentAccount(val size: AccountIconSize = AccountIconSize.Default) : CurrencyIconState() {
override val isGrayscale: Boolean = false
override val shouldShowCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
}
@Immutable
sealed class CryptoPortfolio : CurrencyIconState() {
override val shouldShowCustomBadge: Boolean = false
@ -155,6 +161,7 @@ sealed class CurrencyIconState {
is CryptoPortfolio.Letter -> copy(
isGrayscale = isGrayscale,
)
is PaymentAccount,
is Loading,
is Locked,
is Empty,

View file

@ -9,4 +9,5 @@ data class SearchBarUM(
val isActive: Boolean,
val onActiveChange: (Boolean) -> Unit,
val onClearClick: () -> Unit = {},
val onCancelClick: (() -> Unit)? = null,
)

View file

@ -45,7 +45,7 @@ fun PasteButton(
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty()
val isPasteEnabled = clipboardManager.hasText()
val color = if (isPasteEnabled) {
backgroundColorEnabled
} else {

View file

@ -121,6 +121,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
PriceChangeInPercent(
valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent,
type = marketPriceBlockState.priceChangeConfig.type,
textStyle = TangemTheme.typography.body2,
)
}
} else {

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -13,15 +14,43 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
fun PriceChangeInPercent(
valueInPercent: String,
type: PriceChangeType,
textStyle: TextStyle,
modifier: Modifier = Modifier,
isDisabled: Boolean = false,
) {
if (LocalRedesignEnabled.current) {
PriceChangeInPercentV2(
modifier = modifier,
valueInPercent = valueInPercent,
type = type,
textStyle = textStyle,
isDisabled = isDisabled,
)
} else {
PriceChangeInPercentV1(
modifier = modifier,
valueInPercent = valueInPercent,
type = type,
textStyle = textStyle,
)
}
}
@Composable
private fun PriceChangeInPercentV1(
valueInPercent: String,
type: PriceChangeType,
textStyle: TextStyle,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.body2,
) {
if (valueInPercent.isBlank()) {
Box(modifier)
@ -66,25 +95,87 @@ fun PriceChangeInPercent(
}
}
@Composable
private fun PriceChangeInPercentV2(
valueInPercent: String,
type: PriceChangeType,
textStyle: TextStyle,
isDisabled: Boolean,
modifier: Modifier = Modifier,
) {
if (valueInPercent.isBlank()) {
Box(modifier)
return
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x0_5),
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens2.x3)
.align(Alignment.CenterVertically),
imageVector = ImageVector.vectorResource(
id = when (type) {
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
},
),
tint = if (isDisabled) {
TangemTheme.colors2.graphic.neutral.tertiary
} else {
when (type) {
PriceChangeType.UP -> TangemTheme.colors2.markers.iconBlue
PriceChangeType.DOWN -> TangemTheme.colors2.markers.iconRed
PriceChangeType.NEUTRAL -> TangemTheme.colors2.markers.iconGray
}
},
contentDescription = null,
)
Text(
text = valueInPercent,
color = if (isDisabled) {
TangemTheme.colors2.text.status.disabled
} else {
when (type) {
PriceChangeType.UP -> TangemTheme.colors2.text.status.accent
PriceChangeType.DOWN -> TangemTheme.colors2.text.status.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors2.text.neutral.tertiary
}
},
style = textStyle,
overflow = TextOverflow.Visible,
maxLines = 1,
)
}
}
//region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
private fun PreviewV1() {
TangemThemePreview {
Column {
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.NEUTRAL,
textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.UP,
textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.DOWN,
textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
@ -95,4 +186,38 @@ private fun Preview() {
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
Column {
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.NEUTRAL,
textStyle = TangemTheme.typography2.captionRegular12,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.UP,
textStyle = TangemTheme.typography2.captionRegular12,
isDisabled = true,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.DOWN,
textStyle = TangemTheme.typography2.captionRegular12,
isDisabled = false,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.DOWN,
textStyle = TangemTheme.typography2.captionRegular12,
)
}
}
}
}
//endregion Preview

View file

@ -1,8 +1,11 @@
package com.tangem.core.ui.components.marketprice
sealed class PriceChangeState {
import androidx.compose.runtime.Immutable
data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState()
@Immutable
sealed interface PriceChangeState {
object Unknown : PriceChangeState()
data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState
data object Unknown : PriceChangeState
}

View file

@ -2,7 +2,8 @@ package com.tangem.core.ui.components.tokenlist
import androidx.compose.animation.*
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
import androidx.compose.animation.core.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
@ -98,6 +100,9 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
icon.copy(
size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default,
)
is CurrencyIconState.PaymentAccount -> icon.copy(
size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default,
)
else -> icon
}
@ -183,7 +188,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
}
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun ExpandedPortfolioHeader(
state: TokenItemState,
@ -209,6 +214,7 @@ fun ExpandedPortfolioHeader(
composables.icon.invoke(Modifier)
} else {
when (val icon = state.iconState) {
is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(size = icon.size)
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
resId = icon.resId,
color = icon.color,

View file

@ -55,6 +55,12 @@ sealed interface TokensListItemUM {
is PortfolioItemContentUM.Tokens -> content.tokens
is PortfolioItemContentUM.Empty -> persistentListOf()
}
val tokensItemsList: List<Token>
get() = when (content) {
is PortfolioItemContentUM.Tokens -> content.tokens.filterIsInstance<Token>()
is PortfolioItemContentUM.Empty -> emptyList()
}
}
data class Text(override val id: Any, val text: TextReference) : TokensListItemUM

View file

@ -289,8 +289,13 @@ private fun CancelButton(
state.onQueryChange("")
}
keyboardController?.hide()
state.onClearClick()
focusManager.clearFocus()
val onCancel = state.onCancelClick
if (onCancel != null) {
onCancel()
} else {
state.onClearClick()
focusManager.clearFocus()
}
},
)
}

View file

@ -169,6 +169,7 @@ private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurR
blurRadius = blurRadius,
)
}
is CurrencyIconState.PaymentAccount -> Unit
CurrencyIconState.Loading -> Unit
CurrencyIconState.Locked -> Unit
}
@ -193,7 +194,7 @@ private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) {
AsyncImage(
model = imageRequest,
contentDescription = null,
contentScale = ContentScale.Crop,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.matchParentSize()
.scale(SCALE_FACTOR)
@ -207,7 +208,7 @@ private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) {
Image(
painter = painterResource(res),
contentDescription = null,
contentScale = ContentScale.Crop,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.matchParentSize()
.scale(SCALE_FACTOR)

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object ImportWalletScreenTestTags {
const val PHRASE_TEXT_FIELD = "IMPORT_WALLET_PHRASE_TEXT_FIELD"
const val PASSPHRASE_TEXT_FIELD = "IMPORT_WALLET_PASSPHRASE_TEXT_FIELD"
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19.395,21C21.017,21 22,19.882 22,18.433C22,17.987 21.866,17.523 21.628,17.106L14.223,4.298C13.727,3.436 12.878,3 12,3C11.122,3 10.263,3.436 9.777,4.298L2.372,17.106C2.115,17.533 2,17.987 2,18.433C2,19.882 2.983,21 4.605,21H19.395ZM12.009,14.672C11.513,14.672 11.237,14.387 11.227,13.885L11.103,8.732C11.094,8.229 11.466,7.869 12,7.869C12.515,7.869 12.916,8.239 12.906,8.741L12.763,13.885C12.754,14.397 12.477,14.672 12.009,14.672ZM12.009,17.845C11.437,17.845 10.941,17.39 10.941,16.832C10.941,16.263 11.427,15.808 12.009,15.808C12.582,15.808 13.069,16.254 13.069,16.832C13.069,17.4 12.573,17.845 12.009,17.845Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.294,5.959C9.684,5.569 10.318,5.569 10.708,5.959L14.874,10.126C15.264,10.517 15.264,11.15 14.874,11.54C14.483,11.931 13.85,11.931 13.46,11.54L10,8.081L6.541,11.54C6.15,11.931 5.517,11.931 5.127,11.54C4.736,11.15 4.736,10.517 5.127,10.126L9.294,5.959Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M3.443,6.057C3.111,6.482 2.673,6.326 2.516,5.781L1.727,3.071C1.602,2.639 1.865,2.301 2.316,2.32L5.139,2.414C5.702,2.433 5.953,2.827 5.627,3.246L5.064,3.966L5.089,3.979C6.241,4.742 7.649,6.332 7.987,7.384H8.012C8.344,6.339 9.759,4.742 10.91,3.979L10.942,3.96L10.366,3.165C10.053,2.733 10.309,2.345 10.879,2.351L13.702,2.339C14.153,2.339 14.403,2.689 14.266,3.121L13.389,5.8C13.22,6.339 12.769,6.482 12.45,6.038L11.843,5.199L11.705,5.287C10.447,6.088 8.757,8.06 8.757,9.493V12.842C8.757,13.387 8.488,13.681 8,13.681C7.511,13.681 7.248,13.387 7.248,12.842V9.493C7.248,8.06 5.546,6.088 4.294,5.287L4.125,5.18L3.443,6.057Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.999,1.25C13.68,1.25 15.047,1.818 16.24,2.436C16.6,2.622 16.935,2.807 17.255,2.983C18.038,3.415 18.733,3.797 19.495,4.04C19.942,4.183 20.338,4.31 20.619,4.42C20.883,4.523 21.198,4.665 21.42,4.913C21.621,5.136 21.723,5.385 21.792,5.624C21.854,5.841 21.907,6.135 21.958,6.417C23.196,13.282 20.492,19.787 13.881,22.318C13.232,22.567 12.753,22.75 12.002,22.75C11.251,22.75 10.772,22.567 10.123,22.318C3.512,19.788 0.806,13.283 2.043,6.417C2.094,6.135 2.147,5.841 2.209,5.624C2.277,5.385 2.38,5.135 2.58,4.913C2.802,4.665 3.117,4.524 3.381,4.42C3.662,4.31 4.059,4.183 4.506,4.04C5.268,3.797 5.961,3.415 6.744,2.983C7.063,2.807 7.398,2.622 7.758,2.436C8.95,1.818 10.317,1.25 11.999,1.25ZM17.942,8.666C17.757,8.146 17.186,7.873 16.666,8.058C15.788,8.369 14.948,8.961 14.212,9.607C13.465,10.262 12.766,11.023 12.172,11.734C11.733,12.261 11.345,12.769 11.03,13.199C10.741,12.854 10.453,12.601 10.179,12.418C9.905,12.235 9.552,12 9,12C8.447,12 8,12.448 8,13C8,13.521 8.397,13.949 8.906,13.996C9.11,14.015 9.664,14.566 10.105,15.447C10.266,15.77 10.589,15.981 10.949,15.999C11.309,16.017 11.65,15.84 11.843,15.536C11.849,15.528 12.193,15.012 12.357,14.78C12.688,14.311 13.157,13.677 13.708,13.016C14.262,12.352 14.887,11.675 15.53,11.111C16.183,10.539 16.8,10.132 17.333,9.943C17.854,9.759 18.126,9.187 17.942,8.666Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<group>
<clip-path
android:pathData="M36,0L36,0A36,36 0,0 1,72 36L72,36A36,36 0,0 1,36 72L36,72A36,36 0,0 1,0 36L0,36A36,36 0,0 1,36 0z"/>
<path
android:pathData="M0,0h72v72h-72z"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M24.607,31.142C23.778,32.206 22.682,31.815 22.291,30.454L20.319,23.678C20.006,22.598 20.663,21.753 21.79,21.8L28.848,22.035C30.256,22.081 30.882,23.067 30.069,24.116L28.66,25.915L28.723,25.947C31.602,27.856 35.123,31.831 35.968,34.46H36.031C36.86,31.846 40.397,27.856 43.276,25.947L43.354,25.9L41.915,23.912C41.132,22.833 41.774,21.862 43.198,21.878L50.256,21.847C51.382,21.847 52.008,22.723 51.664,23.803L49.473,30.501C49.051,31.846 47.924,32.206 47.126,31.095L45.608,28.998L45.264,29.217C42.118,31.22 37.893,36.15 37.893,39.734V48.106C37.893,49.467 37.22,50.203 36,50.203C34.779,50.203 34.122,49.467 34.122,48.106V39.734C34.122,36.15 29.865,31.22 26.735,29.217L26.313,28.951L24.607,31.142Z"
android:fillColor="#0099FF"/>
</group>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M28,0L28,0A28,28 0,0 1,56 28L56,28A28,28 0,0 1,28 56L28,56A28,28 0,0 1,0 28L0,28A28,28 0,0 1,28 0z"/>
<path
android:pathData="M0,0h56v56h-56z"
android:strokeAlpha="0.1"
android:fillColor="#FFB71B"
android:fillAlpha="0.1"/>
<path
android:pathData="M41.646,35.377C41.646,37.37 40.305,38.906 38.091,38.906H17.909C15.696,38.906 14.354,37.37 14.354,35.377C14.354,34.765 14.511,34.14 14.862,33.555L24.966,15.95C25.631,14.766 26.802,14.167 28,14.167C29.198,14.167 30.357,14.766 31.034,15.95L41.138,33.555C41.464,34.127 41.646,34.765 41.646,35.377ZM28.013,30.208C27.336,30.208 26.959,29.817 26.946,29.127L26.776,22.044C26.763,21.354 27.271,20.859 28,20.859C28.703,20.859 29.25,21.367 29.237,22.057L29.042,29.127C29.029,29.831 28.651,30.208 28.013,30.208ZM28.013,34.57C27.232,34.57 26.555,33.945 26.555,33.177C26.555,32.396 27.219,31.771 28.013,31.771C28.795,31.771 29.459,32.383 29.459,33.177C29.459,33.958 28.782,34.57 28.013,34.57Z"
android:fillColor="#FFB71B"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.608,1.147C14.314,1.448 14.797,2.163 14.797,3.017L14.798,9.785C14.798,9.895 14.887,9.985 14.998,9.985H18.099C18.985,9.985 19.595,10.583 19.847,11.21C20.097,11.837 20.064,12.642 19.563,13.285L12.564,22.268C12.003,22.988 11.12,23.163 10.392,22.852C9.685,22.551 9.202,21.837 9.202,20.983L9.202,14.215C9.202,14.104 9.112,14.015 9.002,14.015H5.9C5.014,14.015 4.404,13.417 4.153,12.789C3.902,12.163 3.936,11.358 4.437,10.715L11.435,1.732C11.996,1.011 12.879,0.837 13.608,1.147Z"
android:fillColor="#0099FF"/>
</vector>

Some files were not shown because too many files have changed in this diff Show more