Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-02 11:18:27 +02:00
commit 788d7064b5
217 changed files with 6061 additions and 1892 deletions

View file

@ -2,7 +2,7 @@
name: analyze-logs name: analyze-logs
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation. description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
allowed-tools: Read, Grep allowed-tools: Read, Grep
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] [--no-secrets-audit]
--- ---
Analyze the Tangem app user log file at path: `$ARGUMENTS` Analyze the Tangem app user log file at path: `$ARGUMENTS`
@ -108,12 +108,37 @@ Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (dev
- `MainActivity.*onNewIntent` — deep link or push notification - `MainActivity.*onNewIntent` — deep link or push notification
- `CardSDK_Session.*start card session` — NFC session starts - `CardSDK_Session.*start card session` — NFC session starts
**Secrets & PII Audit (full file, head_limit: 20 each, -n: true):**
Skip this entire group if `--no-secrets-audit` is in arguments.
- API key leak in URL: `[?&](api[_-]?key|apiKey|access_token|token|secret)=(?!\*+)[^&\s]{8,}`
- Bearer token: `Bearer\s+[A-Za-z0-9._\-]{20,}`
- JWT: `eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+`
- Authorization header: `(?i)authorization:\s*\S+`
- Critical PII in JSON: `"(privateKey|mnemonic|seedPhrase|private_key|seed_phrase)"\s*:\s*"[^"]+"`
- card_public_key in JSON: `"card_public_key"\s*:\s*"[^"]{40,}"`
- FCM push token: `:APA91[A-Za-z0-9_\-]{100,}`
- xprv/tprv extended private key: `\b[xytzuv]prv[A-Za-z0-9]{100,}`
- Suspicious long hex in URL path: `https?://[^?\s]+/[A-Fa-f0-9]{32,}\b`
- Masking health check: count of `\*{6,}` — if 0 in a build that should mask, flag pipeline broken
**Error filtering:** When processing error results, skip these noisy matches: **Error filtering:** When processing error results, skip these noisy matches:
- `java.io.IOException: Canceled` — normal request cancellation - `java.io.IOException: Canceled` — normal request cancellation
- `HttpException(code=304` — HTTP "Not Modified" - `HttpException(code=304` — HTTP "Not Modified"
- Bare stacktrace lines starting with `\tat` - Bare stacktrace lines starting with `\tat`
- `<-- HTTP FAILED: java.io.IOException: Canceled` - `<-- HTTP FAILED: java.io.IOException: Canceled`
### Step 6.5: Masking Consistency Check
Skip if `--no-secrets-audit` in arguments. Run sequentially after the parallel batch (needs results from the masked-endpoint grep).
1. Grep `https?://[^/\s]+/[^\s*]*\*{6,}` (full file, head_limit: 50) — collect all URLs where a path segment is masked
2. For each unique `host + path-prefix-before-mask`, derive the prefix string
3. For each prefix, Grep the prefix followed by a non-`*` character (`<prefix>[^*\s]`, head_limit: 20)
- If hits found → masking inconsistency: same endpoint has both masked and unmasked variants
- Record the prefix, count of masked hits, count of unmasked hits, first unmasked line number
### Step 7: Deep Dive ### Step 7: Deep Dive
For each significant error found above: For each significant error found above:
@ -207,6 +232,37 @@ Structure your report EXACTLY as follows:
|------|-------|---------| |------|-------|---------|
(chronological: app starts, card sessions, navigation, errors, notable API calls) (chronological: app starts, card sessions, navigation, errors, notable API calls)
## Secrets & PII Audit
Omit this section entirely if `--no-secrets-audit` was passed.
### Health Check
- Total masked tokens (`******`) in log: **N**
- If N = 0 in a build expected to mask, flag: "masking pipeline may be broken"
### Confirmed Leaks (CRITICAL / HIGH)
| Line | Severity | Type | Matched (first 16 chars + `…`) | Context |
|------|----------|------|--------------------------------|---------|
### Masking Inconsistencies
| Endpoint Prefix | Masked Hits | Unmasked Hits | First Unmasked Line |
|-----------------|-------------|---------------|---------------------|
### Suspected Leaks (MEDIUM / LOW)
| Line | Severity | Type | Pattern Matched | Why Suspect |
|------|----------|------|-----------------|-------------|
**Severity legend:**
- **CRITICAL** — private key / mnemonic / xprv in clear text
- **HIGH** — API key / bearer / JWT / card_public_key visible
- **MEDIUM** — push token, card_id, persistent identifiers
- **LOW** — heuristic patterns that may be false positives (tx hash, content hash)
**Output rules:**
- Never include the full matched value — always truncate to 16 chars + `…`
- For LOW severity, add a "Why Suspect" column explaining typical false positives
- Skip matches from these known-public Tangem endpoints: `/v1/coins/settings`, `/v1/geo`, `/v1/currencies`, `/v1/hot_crypto`
## Analysis Summary ## Analysis Summary
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations. (2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.) If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)

View file

@ -36,6 +36,9 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Buy' button is displayed") { step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() } onMainScreen { buyButton.assertIsDisplayed() }
} }
step("Assert 'Add funds' button is displayed") {
onMainScreen { addFundsButton.assertIsDisplayed() }
}
step("Assert 'Swap' button is displayed") { step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() } onMainScreen { swapButton.assertIsDisplayed() }
} }
@ -55,8 +58,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) { fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) {
if (isEnabled) { if (isEnabled) {
step("Assert 'Buy' button is enabled") { step("Assert 'Add funds' button is enabled") {
onMainScreen { buyButton.assertIsEnabled() } onMainScreen { addFundsButton.assertIsEnabled() }
} }
step("Assert 'Swap' button is enabled") { step("Assert 'Swap' button is enabled") {
onMainScreen { swapButton.assertIsEnabled() } onMainScreen { swapButton.assertIsEnabled() }
@ -65,8 +68,8 @@ fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean =
onMainScreen { sellButton.assertIsEnabled() } onMainScreen { sellButton.assertIsEnabled() }
} }
} else { } else {
step("Assert 'Buy' button is not enabled") { step("Assert 'Add funds' button is not enabled") {
onMainScreen { buyButton.assertIsNotEnabled() } onMainScreen { addFundsButton.assertIsNotEnabled() }
} }
step("Assert 'Swap' button is not enabled") { step("Assert 'Swap' button is not enabled") {
onMainScreen { swapButton.assertIsNotEnabled() } onMainScreen { swapButton.assertIsNotEnabled() }

View file

@ -0,0 +1,39 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseSearchBarTestTags
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
/**
* "You receive" token chooser opened from the main-screen "Add funds" button.
*/
class ChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ChooseTokenPageObject>(semanticsProvider = semanticsProvider) {
val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val searchBar: KNode = child {
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
}
fun tokenWithTitle(tokenTitle: String): KNode = child {
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
hasAnyDescendant(withTestTag(TokenElementsTestTags.TOKEN_TITLE))
hasAnyDescendant(withText(tokenTitle))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onChooseTokenScreen(function: ChooseTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,45 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
/**
* "Get token" bottom sheet shown after picking a token in the Add funds flow.
* Contains quick actions (Buy / Receive / ) and the "Go to token" button.
*/
class GetTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<GetTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(BaseBottomSheetTestTags.TITLE)
}
val closeButton: KNode = child {
hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON)
}
// The "Get token" sheet action rows use combinedClickable; the row's testTag lands on a
// separate zero-bounds semantics node that fails assertIsDisplayed. Matching the merged node
// by its title text yields the displayed, clickable row (performClick injects a touch at its
// center, which the row's clickable handles).
val buyButton: KNode = child {
hasText(getResourceString(R.string.common_buy))
}
val receiveButton: KNode = child {
hasText(getResourceString(R.string.common_receive))
}
val goToTokenButton: KNode = child {
hasText(getResourceString(R.string.common_go_to_token))
}
}
internal fun BaseTestCase.onGetTokenBottomSheet(function: GetTokenBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

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

View file

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

View file

@ -40,15 +40,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Assert error notification title is displayed") { step("Assert error notification title is displayed") {
onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() } onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() }
} }
@ -84,15 +87,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Click on 'Confirm' button in 'Dialog'") { step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() } onDialog { confirmButton.clickWithAssertion() }
} }
@ -155,15 +161,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Click on 'Confirm' button in 'Dialog'") { step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() } onDialog { confirmButton.clickWithAssertion() }
} }
@ -238,15 +247,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Click on 'Confirm' button in 'Dialog'") { step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() } onDialog { confirmButton.clickWithAssertion() }
} }
@ -320,15 +332,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Click on 'Confirm' button in 'Dialog'") { step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() } onDialog { confirmButton.clickWithAssertion() }
} }
@ -406,15 +421,18 @@ class BuyTokenTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.clickWithAssertion() } onMainScreen { addFundsButton.clickWithAssertion() }
} }
step("Click on token with name: '$tokenTitle'") { step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen { onChooseTokenScreen {
topAppBarTitle.assertIsDisplayed() topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() tokenWithTitle(tokenTitle).clickWithAssertion()
} }
} }
step("Click on 'Buy' in 'Get token' bottom sheet") {
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
}
step("Click on 'Confirm' button in 'Dialog'") { step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() } onDialog { confirmButton.clickWithAssertion() }
} }

View file

@ -422,17 +422,17 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses() synchronizeAddresses()
} }
step("Assert 'Buy' button is displayed") { step("Assert 'Add funds' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() } onMainScreen { addFundsButton.assertIsDisplayed() }
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.performClick() } onMainScreen { addFundsButton.performClick() }
} }
step("Assert 'Buy' screen title is displayed") { step("Assert 'Choose token' screen title is displayed") {
onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() } onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
} }
step("Assert token with title: '$tokenTitle' is displayed") { step("Assert token with title: '$tokenTitle' is displayed") {
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() } onChooseTokenScreen { tokenWithTitle(tokenTitle).assertIsDisplayed() }
} }
step("Press 'Back' button") { step("Press 'Back' button") {
device.uiDevice.pressBack() device.uiDevice.pressBack()
@ -481,17 +481,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen() openMainScreen()
} }
step("Assert 'Buy' button is displayed") { step("Assert 'Add funds' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() } onMainScreen { addFundsButton.assertIsDisplayed() }
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.performClick() } onMainScreen { addFundsButton.performClick() }
} }
step("Check 'Action is unavailable' dialog") { step("Assert 'Choose token' screen opens (Add funds is always available)") {
checkActionIsUnavailableDialog() onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
} }
step("Click on 'Ok' button") { step("Press 'Back' to return to main screen") {
onDialog { okButton.performClick() } device.uiDevice.pressBack()
waitForIdle()
} }
step("Assert 'Swap' button is displayed") { step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() } onMainScreen { swapButton.assertIsDisplayed() }
@ -538,17 +539,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen() openMainScreen()
} }
step("Assert 'Buy' button is displayed") { step("Assert 'Add funds' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() } onMainScreen { addFundsButton.assertIsDisplayed() }
} }
step("Click on 'Buy' button") { step("Click on 'Add funds' button") {
onMainScreen { buyButton.performClick() } onMainScreen { addFundsButton.performClick() }
} }
step("Check 'Action is unavailable' dialog") { step("Assert 'Choose token' screen opens (Add funds is always available)") {
checkActionIsUnavailableDialog() onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
} }
step("Click on 'Ok' button") { step("Press 'Back' to return to main screen") {
onDialog { okButton.performClick() } device.uiDevice.pressBack()
waitForIdle()
} }
step("Assert 'Swap' button is displayed") { step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() } onMainScreen { swapButton.assertIsDisplayed() }

View file

@ -51,12 +51,15 @@ class HideTokenTest : BaseTestCase() {
dialogContainer.assertIsDisplayed() dialogContainer.assertIsDisplayed()
okButton.clickWithAssertion() okButton.clickWithAssertion()
} }
waitForIdle()
} }
step("Assert token: '$tokenTitle' is not displayed") { step("Assert token: '$tokenTitle' is not displayed") {
flakySafely {
onMainScreen { assertTokenDoesNotExist(tokenTitle) } onMainScreen { assertTokenDoesNotExist(tokenTitle) }
} }
} }
} }
}
@AllureId("3627") @AllureId("3627")
@DisplayName("Main: hide token via Manage tokens") @DisplayName("Main: hide token via Manage tokens")

View file

@ -4,10 +4,12 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.swipeVertical import com.tangem.common.extensions.swipeVertical
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onAddAndManageBottomSheet
import com.tangem.screens.onMainScreen import com.tangem.screens.onMainScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
@ -86,6 +88,15 @@ class MainScreenTest : BaseTestCase() {
step("Assert 'Add & Manage' button is displayed") { step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButtonNode.assertIsDisplayed() } onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
} }
step("Click 'Add & Manage' button") {
onMainScreen { addAndManageButtonNode.clickWithAssertion() }
}
step("Assert 'Organize tokens' option is not displayed (nothing to organize)") {
onAddAndManageBottomSheet { organizeTokensButton.assertIsNotDisplayed() }
}
step("Assert 'Add tokens' option is displayed") {
onAddAndManageBottomSheet { addTokensButton.assertIsDisplayed() }
}
} }
} }

View file

@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>() private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
TangemLogger.i("onCreate") TangemLogger.i("onCreate: data=${intent?.data}, extras=${intent?.extras?.keySet()}")
// We need to call it before onCreate to prevent unnecessary activity recreation // We need to call it before onCreate to prevent unnecessary activity recreation
installAppTheme() installAppTheme()

View file

@ -14,12 +14,14 @@ class AppsFlyerDeepLinkListener @Inject constructor(
override fun onDeepLinking(p0: DeepLinkResult) { override fun onDeepLinking(p0: DeepLinkResult) {
when (p0.status) { when (p0.status) {
DeepLinkResult.Status.FOUND -> { DeepLinkResult.Status.FOUND -> {
referralParamsHandler.handle(deepLink = p0.deepLink) referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
} }
DeepLinkResult.Status.NOT_FOUND -> { DeepLinkResult.Status.NOT_FOUND -> {
referralParamsHandler.handleNoDeeplink()
TangemLogger.i("No deep link found") TangemLogger.i("No deep link found")
} }
DeepLinkResult.Status.ERROR -> { DeepLinkResult.Status.ERROR -> {
referralParamsHandler.handleNoDeeplink()
TangemLogger.e("Deep link error: ${p0.error}") TangemLogger.e("Deep link error: ${p0.error}")
} }
} }

View file

@ -7,6 +7,7 @@ import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -23,14 +24,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
) { ) {
private val mutex = Mutex() private val mutex = Mutex()
private val deepLinkDeferred = CompletableDeferred<String?>()
fun handle(deepLink: DeepLink) {
handle(
deepLinkValue = deepLink.deepLinkValue,
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
)
}
fun handle(params: Map<String?, Any?>) { fun handle(params: Map<String?, Any?>) {
handle( handle(
@ -40,6 +34,31 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
) )
} }
fun handleDeeplink(deepLink: DeepLink) {
handle(
deepLinkValue = deepLink.deepLinkValue,
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
)
deepLinkDeferred.complete(deepLink.deepLinkValue)
}
fun handleNoDeeplink() {
deepLinkDeferred.complete(null)
}
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
val deeplinkFromCache = appsFlyerStore.getDeeplink(deeplinkSource)
return if (deeplinkFromCache == null) {
val value = when (deeplinkSource) {
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
}
deepLinkDeferred.await().takeIf { it == value }
} else {
deeplinkFromCache
}
}
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue") TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue")
when (deepLinkValue) { when (deepLinkValue) {

View file

@ -4,15 +4,20 @@ import android.app.Application
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log import com.tangem.Log
import com.tangem.TangemSdkLogger import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.logs.SensitiveUrlMasker
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.common.LogConfig import com.tangem.domain.common.LogConfig
import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.operations.attestation.api.TangemApiServiceSettings
import com.tangem.utils.JsonStringValuesExtractor
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import kotlinx.serialization.json.Json
/** /**
* Owns all app-startup wiring of the logging subsystem in a single place: * Owns all app-startup wiring of the logging subsystem in a single place:
@ -23,12 +28,15 @@ import com.tangem.wallet.BuildConfig
* @property appLogsStore app logs store used by file-based writer and the network logs save * @property appLogsStore app logs store used by file-based writer and the network logs save
* interceptor * interceptor
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger] * @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
* @property environmentConfig source of [BlockchainSdkConfig] used to build the blockchain
* URL masker
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class TangemLoggingInitializer( class TangemLoggingInitializer(
private val appLogsStore: AppLogsStore, private val appLogsStore: AppLogsStore,
private val tangemSdkLogger: TangemSdkLogger, private val tangemSdkLogger: TangemSdkLogger,
private val environmentConfig: EnvironmentConfig,
) { ) {
fun initAppLogging() { fun initAppLogging() {
@ -64,6 +72,13 @@ class TangemLoggingInitializer(
} }
add(createNetworkLoggingInterceptor()) add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(application)) add(ChuckerInterceptor(application))
add(
NetworkLogsSaveInterceptor(
appLogsStore = appLogsStore,
sensitiveUrlMasker = createBlockchainSensitiveUrlMasker(),
shouldCheckResponseBodySize = true,
),
)
} }
TangemApiServiceSettings.addInterceptors( TangemApiServiceSettings.addInterceptors(
@ -77,4 +92,16 @@ class TangemLoggingInitializer(
}.toTypedArray(), }.toTypedArray(),
) )
} }
private fun createBlockchainSensitiveUrlMasker(): SensitiveUrlMasker {
val json = Json.encodeToJsonElement(
BlockchainSdkConfig.serializer(),
environmentConfig.blockchainSdkConfig,
)
// Drop URL-shaped values (e.g. public endpoint URLs from BlockchainSdkConfig like
// kaspaSecondaryApiUrl); they are not secrets and would obscure unrelated requests in logs.
val values = JsonStringValuesExtractor.extract(json)
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }
return SensitiveUrlMasker(values)
}
} }

View file

@ -5,12 +5,10 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.domain.card.BuildConfig import com.tangem.domain.card.BuildConfig
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
@ -34,8 +32,6 @@ internal class TangemSdkManagerModule {
visaCardActivationTaskFactory: VisaCardActivationTask.Factory, visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles, onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
blockchainToDeriveFinder: BlockchainToDeriveFinder,
analyticsErrorHandler: AnalyticsErrorHandler, analyticsErrorHandler: AnalyticsErrorHandler,
cardRepository: CardRepository, cardRepository: CardRepository,
): TangemSdkManager { ): TangemSdkManager {
@ -49,8 +45,6 @@ internal class TangemSdkManagerModule {
visaCardActivationTaskFactory = visaCardActivationTaskFactory, visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory, tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingV2FeatureToggles = onboardingV2FeatureToggles,
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
blockchainToDeriveFinder = blockchainToDeriveFinder,
analyticsErrorHandler = analyticsErrorHandler, analyticsErrorHandler = analyticsErrorHandler,
cardRepository = cardRepository, cardRepository = cardRepository,
) )

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.data package com.tangem.tap.di.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.tap.common.log.TangemBlockchainSDKLogger import com.tangem.tap.common.log.TangemBlockchainSDKLogger
import com.tangem.tap.common.log.TangemCardSDKLogger import com.tangem.tap.common.log.TangemCardSDKLogger
@ -17,10 +18,14 @@ internal object TangemLoggingModule {
@Provides @Provides
@Singleton @Singleton
fun provideLoggingInitializer(appLogsStore: AppLogsStore): TangemLoggingInitializer { fun provideLoggingInitializer(
appLogsStore: AppLogsStore,
environmentConfig: EnvironmentConfig,
): TangemLoggingInitializer {
return TangemLoggingInitializer( return TangemLoggingInitializer(
appLogsStore = appLogsStore, appLogsStore = appLogsStore,
tangemSdkLogger = TangemCardSDKLogger(appLogsStore), tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
environmentConfig = environmentConfig,
) )
} }

View file

@ -27,7 +27,6 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
@ -58,6 +57,7 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -73,8 +73,6 @@ internal class DefaultTangemSdkManager(
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
private val analyticsErrorHandler: AnalyticsErrorHandler, private val analyticsErrorHandler: AnalyticsErrorHandler,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
) : TangemSdkManager { ) : TangemSdkManager {
@ -145,12 +143,10 @@ internal class DefaultTangemSdkManager(
runTaskAsyncReturnOnMain( runTaskAsyncReturnOnMain(
runnable = ScanProductTask( runnable = ScanProductTask(
card = null, card = null,
blockchainToDeriveFinder = blockchainToDeriveFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler, visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this, visaCoroutineScope = this,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingV2FeatureToggles = onboardingV2FeatureToggles,
cardRepository = cardRepository, cardRepository = cardRepository,
), ),
@ -242,6 +238,7 @@ internal class DefaultTangemSdkManager(
Analytics.send(event = analyticsEvent.withParams(params.toMap())) Analytics.send(event = analyticsEvent.withParams(params.toMap()))
} }
.doOnFailure { tangemError -> .doOnFailure { tangemError ->
TangemLogger.e("scanProduct failed: code=${tangemError.code}, message=${tangemError.customMessage}")
(tangemError as? TangemSdkError)?.let { error -> (tangemError as? TangemSdkError)?.let { error ->
Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
} }
@ -470,7 +467,6 @@ internal class DefaultTangemSdkManager(
runnable = FinalizeTwinTask( runnable = FinalizeTwinTask(
twinPublicKey = secondCardPublicKey, twinPublicKey = secondCardPublicKey,
issuerKeys = issuerKeyPair, issuerKeys = issuerKeyPair,
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
cardRepository = cardRepository, cardRepository = cardRepository,
), ),
cardId = cardId, cardId = cardId,

View file

@ -1,74 +0,0 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.wallets.derivations.BlockchainToDerive
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.tap.features.demo.DemoHelper
import javax.inject.Inject
/**
* Finder of blockchains to derive.
* Returns only saved, default or demo blockchains without any additional logic
* (no cardano/ethereum additions or unnecessary blockchain removals).
*/
class BlockchainToDeriveFinder @Inject constructor(
private val walletAccountsFetcher: WalletAccountsFetcher,
) {
suspend fun find(card: CardDTO): Set<BlockchainToDerive> {
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
val derivationStyle = card.derivationStyleProvider.getDerivationStyle()
val blockchains = getBlockchains(userWalletId).ifEmpty {
if (DemoHelper.isDemoCardId(card.cardId)) {
getDemoBlockchains(derivationStyle, card.cardId)
} else {
getDefaultBlockchains(derivationStyle)
}
}
return blockchains
}
private suspend fun getBlockchains(userWalletId: UserWalletId): Set<BlockchainToDerive> {
return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty()
.flatMap { accountDTO ->
accountDTO.tokens.orEmpty()
.filter { it.contractAddress == null }
}
.mapNotNull { coin ->
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null
BlockchainToDerive(blockchain, derivationPath)
}
.toSet()
}
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): Set<BlockchainToDerive> {
return mapNotNullTo(hashSetOf()) { blockchain ->
val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
BlockchainToDerive(blockchain, derivationPath)
}
}
}

View file

@ -12,8 +12,6 @@ import com.tangem.common.extensions.*
import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
@ -32,25 +30,21 @@ import com.tangem.operations.PreflightReadMode
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.tap.mainScope import com.tangem.tap.mainScope
import com.tangem.tap.scope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal class ScanProductTask( internal class ScanProductTask(
private val card: Card?, private val card: Card?,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val visaCardScanHandler: VisaCardScanHandler?, private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?, private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
private val shouldCheckIsAlreadyActivated: Boolean, private val shouldCheckIsAlreadyActivated: Boolean,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
override val allowsRequestAccessCodeFromRepository: Boolean = false, override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> { ) : CardSessionRunnable<ScanResponse> {
@ -80,8 +74,6 @@ internal class ScanProductTask(
session = session, session = session,
cardDto = cardDto, cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor( scanWalletProcessor = ScanWalletProcessor(
blockchainToDeriveFinder = blockchainToDeriveFinder,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
cardRepository = cardRepository, cardRepository = cardRepository,
), ),
callback = callback, callback = callback,
@ -92,8 +84,6 @@ internal class ScanProductTask(
val commandProcessor = when { val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor() cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor( else -> ScanWalletProcessor(
blockchainToDeriveFinder = blockchainToDeriveFinder,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
cardRepository = cardRepository, cardRepository = cardRepository,
) )
} }
@ -102,8 +92,8 @@ internal class ScanProductTask(
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult -> is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) { when (scanTaskResult) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
// it needed because processorResult.data.card doesn't contains attestation result // It's needed because processorResult.data.card doesn't contain the attestation
// and CardWallet.derivedKeys // result or the existing CardWallet.derivedKeys read from the card.
val processorScanResponseWithNewCard = processorResult.data.copy( val processorScanResponseWithNewCard = processorResult.data.copy(
card = CardDTO(scanTaskResult.data), card = CardDTO(scanTaskResult.data),
) )
@ -176,8 +166,6 @@ internal class ScanProductTask(
} }
private class ScanWalletProcessor( private class ScanWalletProcessor(
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
) : ProductCommandProcessor<ScanResponse> { ) : ProductCommandProcessor<ScanResponse> {
@ -281,48 +269,34 @@ private class ScanWalletProcessor(
when (linkingResult) { when (linkingResult) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
primaryCard = linkingResult.data primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback) completeScan(card, session, callback)
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback) completeScan(card, session, callback)
} }
} }
} }
} else { } else {
deriveKeysIfNeeded(card, session, callback) completeScan(card, session, callback)
} }
} }
} }
private fun deriveKeysIfNeeded( // Keys are no longer derived during scan: default derivations are created up front in
// CreateProductWalletTask, and derivations for additional tokens are handled by
// DefaultColdMapDerivationsRepository when the user explicitly adds a token.
private fun completeScan(
card: CardDTO, card: CardDTO,
session: CardSession, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit, callback: (result: CompletionResult<ScanResponse>) -> Unit,
) { ) {
val productType = getWalletProductType(card)
scope.launch {
val scanResponse = ScanResponse( val scanResponse = ScanResponse(
card = card, card = card,
productType = productType, productType = getWalletProductType(card),
walletData = session.environment.walletData, walletData = session.environment.walletData,
primaryCard = primaryCard, primaryCard = primaryCard,
) )
val derivations = collectDerivations(card, scanResponse)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse)) callback(CompletionResult.Success(scanResponse))
return@launch
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = scanResponse.copy(derivedKeys = result.data.entries)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
} }
private fun getWalletProductType(card: CardDTO): ProductType { private fun getWalletProductType(card: CardDTO): ProductType {
@ -334,17 +308,6 @@ private class ScanWalletProcessor(
else -> ProductType.Wallet else -> ProductType.Wallet
} }
} }
private suspend fun collectDerivations(
card: CardDTO,
scanResponse: ScanResponse,
): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = blockchainToDeriveFinder
?.find(card)
?: return emptyMap()
return MissedDerivationsFinder(scanResponse, isDynamicAddressesEnabled).findByBlockchainsToDerive(blockchains)
}
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")

View file

@ -13,7 +13,6 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
class FinalizeTwinTask( class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val twinPublicKey: ByteArray,
private val issuerKeys: KeyPair, private val issuerKeys: KeyPair,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
) : CardSessionRunnable<ScanResponse> { ) : CardSessionRunnable<ScanResponse> {
@ -31,11 +30,9 @@ class FinalizeTwinTask(
is CompletionResult.Success -> is CompletionResult.Success ->
ScanProductTask( ScanProductTask(
card = readResult.data, card = readResult.data,
blockchainToDeriveFinder = null,
visaCardScanHandler = null, visaCardScanHandler = null,
visaCoroutineScope = null, visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false, shouldCheckIsAlreadyActivated = false,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
onboardingV2FeatureToggles = null, onboardingV2FeatureToggles = null,
cardRepository = cardRepository, cardRepository = cardRepository,
).run(session, callback) ).run(session, callback)

View file

@ -325,11 +325,7 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(listOf(encryptionKey)) sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> .doOnSuccess { sensitiveInfo ->
updateWallets { wallets -> updateWallets { wallets ->
// It is necessary to update derivations because when scanning we obtain the missing keys wallets?.updateWith(walletIdToSensitiveInformation = sensitiveInfo)
wallets?.updateWith(
walletIdToSensitiveInformation = sensitiveInfo,
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
)
} }
trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card) trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card)
} }

View file

@ -1,10 +1,8 @@
package com.tangem.tap.domain.userWalletList.utils package com.tangem.tap.domain.userWalletList.utils
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
@ -74,10 +72,7 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
return this.map { it.toUserWallet() } return this.map { it.toUserWallet() }
} }
internal fun UserWallet.updateWith( internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
sensitiveInformation: UserWalletSensitiveInformation,
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
): UserWallet {
return when (this) { return when (this) {
is UserWallet.Cold -> { is UserWallet.Cold -> {
copy( copy(
@ -85,7 +80,6 @@ internal fun UserWallet.updateWith(
card = scanResponse.card.copy( card = scanResponse.card.copy(
wallets = requireNotNull(sensitiveInformation.wallets), wallets = requireNotNull(sensitiveInformation.wallets),
), ),
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus, // visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
), ),
) )
@ -98,17 +92,14 @@ internal fun UserWallet.updateWith(
internal fun List<UserWallet>.updateWith( internal fun List<UserWallet>.updateWith(
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>, walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
): List<UserWallet> { ): List<UserWallet> {
return if (walletIdToSensitiveInformation.isEmpty()) { return if (walletIdToSensitiveInformation.isEmpty()) {
this this
} else { } else {
this.map { wallet -> this.map { wallet ->
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId] val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
if (sensitiveInformation != null) { if (sensitiveInformation != null) {
wallet.updateWith(sensitiveInformation, derivedKeys) wallet.updateWith(sensitiveInformation)
} else { } else {
wallet wallet
} }

View file

@ -32,7 +32,6 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
@ -54,6 +53,7 @@ import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.android.create import com.tangem.hot.sdk.android.create
import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerReferralParamsHandler
import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.features.root.RootDetectedWarningComponent
import com.tangem.tap.features.scanfails.ScanFailsComponent import com.tangem.tap.features.scanfails.ScanFailsComponent
@ -70,6 +70,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@Suppress("LongParameterList", "LargeClass") @Suppress("LongParameterList", "LargeClass")
internal class DefaultRoutingComponent @AssistedInject constructor( internal class DefaultRoutingComponent @AssistedInject constructor(
@ -88,7 +90,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
private val appsFlyerStore: AppsFlyerStore, private val appsFlyerReferralParamsHandler: AppsFlyerReferralParamsHandler,
private val trackingContextProxy: TrackingContextProxy, private val trackingContextProxy: TrackingContextProxy,
private val scanFailsComponentFactory: ScanFailsComponent.Factory, private val scanFailsComponentFactory: ScanFailsComponent.Factory,
private val scanFailsRequesterProxy: ScanFailsRequesterProxy, private val scanFailsRequesterProxy: ScanFailsRequesterProxy,
@ -212,11 +214,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING, FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
) )
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled") TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
if (isHotWalletOnboardingEnabled) { if (isHotWalletOnboardingEnabled) {
val tangemPayHotWalletOnboardingDeepLink = appsFlyerStore.getDeeplink( val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
) }
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}") TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
if (tangemPayHotWalletOnboardingDeepLink != null) { if (tangemPayHotWalletOnboardingDeepLink != null) {
val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding

View file

@ -9,7 +9,6 @@ import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
@ -116,7 +115,6 @@ internal class ChildFactory @Inject constructor(
private val surveyComponentFactory: SurveyComponent.Factory, private val surveyComponentFactory: SurveyComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
) { ) {
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@ -237,13 +235,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = buyCryptoComponentFactory, componentFactory = buyCryptoComponentFactory,
) )
} }
is AppRoute.AddFunds -> {
createComponentChild(
context = context,
params = AddFundsComponent.Params(userWalletId = route.userWalletId),
componentFactory = addFundsComponentFactory,
)
}
is AppRoute.SellCrypto -> { is AppRoute.SellCrypto -> {
createComponentChild( createComponentChild(
context = context, context = context,

View file

@ -29,15 +29,19 @@ class AppsFlyerDeepLinkListenerTest {
@ProvideTestModels @ProvideTestModels
fun onDeepLinking(model: OnDeepLinkingModel) = runTest { fun onDeepLinking(model: OnDeepLinkingModel) = runTest {
if (model.shouldHandle) { if (model.shouldHandle) {
every { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } just Runs every { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) } just Runs
} else {
every { referralParamsHandler.handleNoDeeplink() } just Runs
} }
listener.onDeepLinking(p0 = model.deepLinkResult) listener.onDeepLinking(p0 = model.deepLinkResult)
if (model.shouldHandle) { if (model.shouldHandle) {
coVerify { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } coVerify { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) }
verify(inverse = true) { referralParamsHandler.handleNoDeeplink() }
} else { } else {
coVerify(inverse = true) { referralParamsHandler.handle(deepLink = any()) } coVerify(inverse = true) { referralParamsHandler.handleDeeplink(deepLink = any()) }
verify { referralParamsHandler.handleNoDeeplink() }
} }
} }

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.appsflyer package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink import com.appsflyer.deeplink.DeepLink
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
@ -15,6 +17,7 @@ import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
@ -46,7 +49,7 @@ class AppsFlyerReferralParamsHandlerTest {
@ParameterizedTest @ParameterizedTest
@ProvideTestModels @ProvideTestModels
fun handle(model: HandleDeepLinkModel) = runTest { fun handle(model: HandleDeepLinkModel) = runTest {
handler.handle(deepLink = model.deepLink) handler.handleDeeplink(deepLink = model.deepLink)
if (model.shouldStore) { if (model.shouldStore) {
val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN)
@ -165,6 +168,78 @@ class AppsFlyerReferralParamsHandlerTest {
data class HandleParamsModel(val params: Map<String?, Any?>, val shouldStore: Boolean) data class HandleParamsModel(val params: Map<String?, Any?>, val shouldStore: Boolean)
@Nested
inner class WaitForDeeplink {
private val localStore: AppsFlyerStore = mockk(relaxUnitFun = true)
private val localHandler = AppsFlyerReferralParamsHandler(
appsFlyerStore = localStore,
coroutineScope = TestAppCoroutineScope(),
setShouldShowMobileWalletPromoUseCase = mockk { coEvery { this@mockk.invoke(true) } returns Unit.right() },
)
@Test
fun `GIVEN cached deeplink WHEN waitForDeeplink THEN returns cached value`() = runTest {
// GIVEN
coEvery {
localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
} returns "tpay_mobileonboard"
// WHEN
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
// THEN
assertThat(result).isEqualTo("tpay_mobileonboard")
}
@Test
fun `GIVEN no cache and matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns deeplink value`() = runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
val deepLink = mockk<DeepLink> {
every { deepLinkValue } returns "tpay_mobileonboard"
every { getStringValue(any()) } returns null
}
// WHEN
localHandler.handleDeeplink(deepLink)
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
// THEN
assertThat(result).isEqualTo("tpay_mobileonboard")
}
@Test
fun `GIVEN no cache and non-matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns null`() = runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
val deepLink = mockk<DeepLink> {
every { deepLinkValue } returns "referral"
every { getStringValue(any()) } returns null
}
// WHEN
localHandler.handleDeeplink(deepLink)
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN no cache WHEN handleNoDeeplink then waitForDeeplink THEN returns null`() = runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
// WHEN
localHandler.handleNoDeeplink()
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
// THEN
assertThat(result).isNull()
}
}
private companion object Companion { private companion object Companion {
const val SUCCESS_REFCODE = "valid_refcode" const val SUCCESS_REFCODE = "valid_refcode"
const val SUCCESS_CAMPAIGN = "valid_campaign" const val SUCCESS_CAMPAIGN = "valid_campaign"

View file

@ -1,252 +0,0 @@
package com.tangem.tap.domain.tasks.product
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.wallets.derivations.BlockchainToDerive
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class BlockchainToDeriveFinderTest {
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
private val finder = BlockchainToDeriveFinder(
walletAccountsFetcher = walletAccountsFetcher,
)
@AfterEach
fun tearDown() {
clearMocks(walletAccountsFetcher)
}
@Test
fun `GIVEN card is not HD wallet THEN return empty set`() = runTest {
// Arrange
val card = mockk<CardDTO> {
every { this@mockk.settings.isHDWalletAllowed } returns false
}
// Act
val actual = finder.find(card)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `GIVEN card has empty wallets THEN return empty set`() = runTest {
// Arrange
val card = mockk<CardDTO> {
every { this@mockk.settings.isHDWalletAllowed } returns true
every { this@mockk.wallets } returns emptyList()
}
// Act
val actual = finder.find(card)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest {
// Arrange
val card = createCardDTO()
val response = createResponse(Blockchain.Bitcoin)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest {
// Arrange
val demoCardId = "AC01000000045754"
val card = createCardDTO(cardId = demoCardId)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
createExpected(Blockchain.Dogecoin),
createExpected(Blockchain.Solana),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest {
// Arrange
val demoCardId = "DE00"
val card = createCardDTO(cardId = demoCardId)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
createExpected(Blockchain.Dogecoin),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store THEN return default blockchains`() = runTest {
// Arrange
val card = createCardDTO()
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN saved cardano THEN return only cardano`() = runTest {
// Arrange
val card = createCardDTO()
val response = createResponse(Blockchain.Cardano)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Cardano),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest {
// Arrange
val card = createCardDTO()
val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon)
val response = createResponse(*blockchains.toTypedArray())
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = blockchains.mapTo(hashSetOf(), ::createExpected)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO {
val wallet = mockk<CardDTO.Wallet> {
every { this@mockk.publicKey } returns byteArrayOf(0)
}
return mockk<CardDTO> {
every { this@mockk.cardId } returns cardId
every { this@mockk.batchId } returns batchId
every { this@mockk.settings.isHDWalletAllowed } returns true
every { this@mockk.settings.isKeysImportAllowed } returns true
every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion(
major = 6,
minor = 33,
patch = 0,
type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release,
)
every { this@mockk.wallets } returns listOf(wallet)
}
}
private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse {
val tokens = blockchains.map { blockchain ->
mockk<UserTokensResponse.Token> {
every { this@mockk.networkId } returns blockchain.toNetworkId()
every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath
every { this@mockk.contractAddress } returns null
}
}
val account = mockk<WalletAccountDTO> {
every { this@mockk.tokens } returns tokens
}
return mockk {
every { this@mockk.accounts } returns listOf(account)
}
}
private fun createExpected(
blockchain: Blockchain,
derivationPath: DerivationPath = blockchain.getDerivationPath(),
): BlockchainToDerive {
return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath)
}
private fun Blockchain.getDerivationPath(): DerivationPath {
return derivationPath(DerivationStyle.V3)!!
}
private companion object {
// for byteArrayOf(0)
val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7")
}
}

View file

@ -59,11 +59,6 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data object Wallet : AppRoute(path = "/wallet") data object Wallet : AppRoute(path = "/wallet")
@Serializable
data class AddFunds(
val userWalletId: UserWalletId,
) : AppRoute(path = "/add_funds/${userWalletId.stringValue}")
@Serializable @Serializable
data class CurrencyDetails( data class CurrencyDetails(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,

View file

@ -34,7 +34,7 @@ class TokenActionsHandler @AssistedInject constructor(
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
@Assisted private val currentAppCurrency: Provider<AppCurrency>, @Assisted private val currentAppCurrency: Provider<AppCurrency>,
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, @Assisted private val onHandleQuickAction: (action: HandledQuickAction, shouldDismiss: Boolean) -> Unit,
private val isDemoCardUseCase: IsDemoCardUseCase, private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
) { ) {
@ -49,6 +49,18 @@ class TokenActionsHandler @AssistedInject constructor(
action = action, action = action,
cryptoCurrencyData = cryptoCurrencyData, cryptoCurrencyData = cryptoCurrencyData,
), ),
when (action) {
TokenActionsBSContentUM.Action.Receive,
TokenActionsBSContentUM.Action.CopyAddress,
TokenActionsBSContentUM.Action.Sell,
-> false
TokenActionsBSContentUM.Action.Send,
TokenActionsBSContentUM.Action.Stake,
TokenActionsBSContentUM.Action.YieldMode,
TokenActionsBSContentUM.Action.Buy,
TokenActionsBSContentUM.Action.Exchange,
-> true
},
) )
val userWallet = cryptoCurrencyData.userWallet val userWallet = cryptoCurrencyData.userWallet
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
@ -164,7 +176,7 @@ class TokenActionsHandler @AssistedInject constructor(
interface Factory { interface Factory {
fun create( fun create(
currentAppCurrency: Provider<AppCurrency>, currentAppCurrency: Provider<AppCurrency>,
onHandleQuickAction: (HandledQuickAction) -> Unit, onHandleQuickAction: (HandledQuickAction, shouldDismiss: Boolean) -> Unit,
): TokenActionsHandler ): TokenActionsHandler
} }

View file

@ -122,5 +122,9 @@
{ {
"name": "AND_15258_QUICK_TOP_UP_ENABLED", "name": "AND_15258_QUICK_TOP_UP_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "AND_15368_VISA_PAY_REDESIGN",
"version": "undefined"
} }
] ]

View file

@ -95,6 +95,7 @@ dependencies {
implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.coroutines.rx2) implementation(deps.kotlin.coroutines.rx2)
implementation(deps.kotlin.datetime) implementation(deps.kotlin.datetime)
implementation(deps.kotlin.serialization)
/** Logging */ /** Logging */

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.api.auth.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* RFC 9457 / RFC 7807 Problem Details response. Returned by Tangem Auth Service with
* `Content-Type: application/problem+json` on every 4xx / 5xx response.
*/
@JsonClass(generateAdapter = true)
data class ProblemDetailResponse(
/** URI identifying the problem type. */
@Json(name = "type") val type: String,
/** Short human-readable summary (e.g. `"Too Many Requests"`). */
@Json(name = "title") val title: String,
/** HTTP status code. */
@Json(name = "status") val status: Int,
/** Human-readable explanation. */
@Json(name = "detail") val detail: String?,
/** URI reference to this occurrence (e.g. `"/api/v1/auth/refresh"`). */
@Json(name = "instance") val instance: String?,
/** Application-specific error code. */
@Json(name = "code") val code: String?,
/** Retry delay for rate limiting (`429`). */
@Json(name = "retryAfterSeconds") val retryAfterSeconds: Int?,
)

View file

@ -97,6 +97,12 @@ interface TangemPayApi {
@Body body: ReissueCardRequest, @Body body: ReissueCardRequest,
): ApiResponse<ReissueCardResponse> ): ApiResponse<ReissueCardResponse>
@POST("v1/customer/card/close")
suspend fun closeCard(
@Header("Authorization") authHeader: String,
@Body body: CloseCardRequest,
): ApiResponse<CloseCardResponse>
@POST("v1/customer/card/withdraw/data") @POST("v1/customer/card/withdraw/data")
suspend fun getWithdrawData( suspend fun getWithdrawData(
@Header("Authorization") authHeader: String, @Header("Authorization") authHeader: String,

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 CloseCardRequest(
@Json(name = "card_id") val cardId: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CloseCardResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: OrderResponse.Result.Status,
)
}

View file

@ -11,7 +11,6 @@ data class YieldBoostStatusResponse(
@Json(name = "userAddress") val userAddress: String?, @Json(name = "userAddress") val userAddress: String?,
@Json(name = "contractAddress") val contractAddress: String?, @Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "promoEnrollmentStatus") val promoEnrollmentStatus: String, @Json(name = "promoEnrollmentStatus") val promoEnrollmentStatus: String,
@Json(name = "activationDate") val activationDate: String?,
@Json(name = "qualificationEndDate") val qualificationEndDate: String?, @Json(name = "qualificationEndDate") val qualificationEndDate: String?,
@Json(name = "disqualificationReason") val disqualificationReason: String?, @Json(name = "disqualificationReason") val disqualificationReason: String?,
) )

View file

@ -3,8 +3,10 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.DefaultTangemPayCloseCardStore
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
import com.tangem.datasource.local.visa.TangemPayReissueCardStore import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -32,4 +34,12 @@ internal object TangemPayStoresModule {
prefs = prefs, prefs = prefs,
) )
} }
@Provides
@Singleton
fun provideTangemPayCloseCardStore(prefs: AppPreferencesStore): TangemPayCloseCardStore {
return DefaultTangemPayCloseCardStore(
prefs = prefs,
)
}
} }

View file

@ -16,11 +16,15 @@ import com.tangem.datasource.api.utils.ConnectTimeout
import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.utils.WriteTimeout import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.logs.SensitiveUrlMasker
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addHeaders
import com.tangem.utils.JsonStringValuesExtractor
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Invocation import retrofit2.Invocation
@ -41,6 +45,7 @@ import javax.inject.Singleton
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Suppress("LongParameterList")
@Singleton @Singleton
internal class RetrofitApiBuilder @Inject constructor( internal class RetrofitApiBuilder @Inject constructor(
private val apiConfigs: ApiConfigs, private val apiConfigs: ApiConfigs,
@ -49,10 +54,20 @@ internal class RetrofitApiBuilder @Inject constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler, private val analyticsErrorHandler: AnalyticsErrorHandler,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val appLogsStore: AppLogsStore, private val appLogsStore: AppLogsStore,
private val environmentConfig: EnvironmentConfig,
) { ) {
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls() private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
private val sensitiveUrlMasker: SensitiveUrlMasker by lazy {
val json = Json.encodeToJsonElement(EnvironmentConfig.serializer(), environmentConfig)
// Drop URL-shaped values (e.g. public endpoint URLs from config); they are not secrets
// and would obscure unrelated requests in logs.
val values = JsonStringValuesExtractor.extract(json)
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }
SensitiveUrlMasker(values)
}
/** /**
* Builds a Retrofit API instance for the specified API configuration ID * Builds a Retrofit API instance for the specified API configuration ID
* *
@ -179,7 +194,7 @@ internal class RetrofitApiBuilder @Inject constructor(
private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder { private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder {
return addInterceptor( return addInterceptor(
interceptor = NetworkLogsSaveInterceptor(appLogsStore), interceptor = NetworkLogsSaveInterceptor(appLogsStore, sensitiveUrlMasker),
) )
} }

View file

@ -4,7 +4,10 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.datasource.local.config.environment.models.ExpressModel import com.tangem.datasource.local.config.environment.models.ExpressModel
import com.tangem.datasource.local.config.environment.models.P2PKeys import com.tangem.datasource.local.config.environment.models.P2PKeys
import com.tangem.datasource.local.config.environment.models.SurveySparrowSwapRatingConfig import com.tangem.datasource.local.config.environment.models.SurveySparrowSwapRatingConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class EnvironmentConfig( data class EnvironmentConfig(
val moonPayApiKey: String = "", val moonPayApiKey: String = "",
val moonPayApiSecretKey: String = "", val moonPayApiSecretKey: String = "",
@ -32,6 +35,7 @@ data class EnvironmentConfig(
val gaslessTxApiKey: String? = null, val gaslessTxApiKey: String? = null,
val customerIoCdpApiKey: String? = null, val customerIoCdpApiKey: String? = null,
val surveySparrowToken: String? = null, val surveySparrowToken: String? = null,
@Transient
val surveySparrowSwapRating: SurveySparrowSwapRatingConfig? = null, val surveySparrowSwapRating: SurveySparrowSwapRatingConfig? = null,
val authServiceKey: String? = null, val authServiceKey: String? = null,
) )

View file

@ -1,7 +1,11 @@
package com.tangem.datasource.local.config.environment.models package com.tangem.datasource.local.config.environment.models
import kotlinx.serialization.Serializable
@Serializable
data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String) data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String)
@Serializable
data class P2PKeys(val mainnet: String, val hoodi: String) data class P2PKeys(val mainnet: String, val hoodi: String)
data class SurveySparrowSwapRatingConfig( data class SurveySparrowSwapRatingConfig(

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.local.logs
class SensitiveUrlMasker(sensitiveValues: Collection<String>) {
// Sorted by descending length so a value that is a prefix of another (e.g. "my-node" vs
// "my-node-prod") cannot mask the shorter one first and leave the suffix in the log.
private val sensitiveValues: List<String> = sensitiveValues
.distinct()
.sortedByDescending(String::length)
fun mask(url: String): String {
var result = url
for (value in sensitiveValues) {
if (result.contains(value, ignoreCase = true)) {
result = result.replace(value, MASKED_VALUE, ignoreCase = true)
}
}
return result
}
companion object {
const val MASKED_VALUE = "******"
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.visa
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
internal class DefaultTangemPayCloseCardStore(
private val prefs: AppPreferencesStore,
) : TangemPayCloseCardStore {
override suspend fun setCloseOrderId(cardId: String, orderId: String?) {
if (orderId == null) {
prefs.edit { it.remove(getCloseKey(cardId)) }
} else {
prefs.store(
key = getCloseKey(cardId),
value = orderId,
)
}
}
override suspend fun getOrderId(cardId: String): String? {
return prefs.getSyncOrNull(key = getCloseKey(cardId))
}
private fun getCloseKey(cardId: String) = stringPreferencesKey("tangem_pay_close_card_$cardId")
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.visa
interface TangemPayCloseCardStore {
suspend fun setCloseOrderId(cardId: String, orderId: String?)
suspend fun getOrderId(cardId: String): String?
}

View file

@ -45,6 +45,7 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "deposit_address") val depositAddress: String?, @Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal, @Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
@Json(name = "cards") val cards: List<TangemPayCard>, @Json(name = "cards") val cards: List<TangemPayCard>,
) : PaymentAccountStatusValueDM ) : PaymentAccountStatusValueDM
@ -58,6 +59,7 @@ sealed interface PaymentAccountStatusValueDM {
@NameLabel("deactivated_account") @NameLabel("deactivated_account")
data class DeactivatedAccount( data class DeactivatedAccount(
@Json(name = "deactivated_account") val marker: Boolean = true, @Json(name = "deactivated_account") val marker: Boolean = true,
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
) : PaymentAccountStatusValueDM ) : PaymentAccountStatusValueDM
@ -86,6 +88,6 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?, @Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "frozen_state") val frozenState: String, @Json(name = "frozen_state") val frozenState: String,
@Json(name = "last_digits") val lastDigits: String, @Json(name = "last_digits") val lastDigits: String,
@Json(name = "is_reissuing") val isReissuing: Boolean, @Json(name = "state") val state: String,
) )
} }

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.utils package com.tangem.datasource.utils
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.logs.SensitiveUrlMasker
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -22,11 +24,15 @@ private const val JSON_INDENT_SPACES = 4
* Interceptor for save network requests and responses logs * Interceptor for save network requests and responses logs
* *
* @property appLogsStore app logs store * @property appLogsStore app logs store
* @property sensitiveUrlMasker masker for sensitive data in URLs
* @property shouldCheckResponseBodySize whether to skip logging large response bodies
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class NetworkLogsSaveInterceptor( class NetworkLogsSaveInterceptor(
private val appLogsStore: AppLogsStore, private val appLogsStore: AppLogsStore,
private val sensitiveUrlMasker: SensitiveUrlMasker? = null,
private val shouldCheckResponseBodySize: Boolean = false,
) : Interceptor { ) : Interceptor {
@Throws(IOException::class) @Throws(IOException::class)
@ -65,7 +71,7 @@ class NetworkLogsSaveInterceptor(
val connection = chain.connection() val connection = chain.connection()
val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" val connectionProtocol = if (connection != null) " ${connection.protocol()}" else ""
saveLogMessage("--> ${request.method} ${request.url}$connectionProtocol\n") saveLogMessage("--> ${request.method} ${request.url.maskSensitiveInfo()}$connectionProtocol\n")
} }
private fun logRequestMessage(chain: Interceptor.Chain, request: Request) { private fun logRequestMessage(chain: Interceptor.Chain, request: Request) {
@ -73,7 +79,7 @@ class NetworkLogsSaveInterceptor(
val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" val connectionProtocol = if (connection != null) " ${connection.protocol()}" else ""
saveLogMessage( saveLogMessage(
"--> ${request.method} ${request.url}$connectionProtocol\n", "--> ${request.method} ${request.url.maskSensitiveInfo()}$connectionProtocol\n",
createRequestEndMessage(request), createRequestEndMessage(request),
) )
} }
@ -110,7 +116,7 @@ class NetworkLogsSaveInterceptor(
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
saveLogMessage( saveLogMessage(
"<-- ${response.code}", "<-- ${response.code}",
" ${response.request.url} (${tookMs}ms)\n", " ${response.request.url.maskSensitiveInfo()} (${tookMs}ms)\n",
) )
} }
@ -123,11 +129,16 @@ class NetworkLogsSaveInterceptor(
"<-- END HTTP" "<-- END HTTP"
} else if (bodyHasUnknownEncoding(response.headers)) { } else if (bodyHasUnknownEncoding(response.headers)) {
"<-- END HTTP (encoded body omitted)" "<-- END HTTP (encoded body omitted)"
} else if (shouldCheckResponseBodySize && contentLength > WRITE_LOG_THRESHOLD_BYTES_SIZE) {
"Response size too large: $contentLength bytes \n<-- END HTTP"
} else { } else {
val source = responseBody.source() val source = responseBody.source()
source.request(Long.MAX_VALUE) source.request(Long.MAX_VALUE)
var buffer = source.buffer var buffer = source.buffer
if (shouldCheckResponseBodySize && buffer.size > WRITE_LOG_THRESHOLD_BYTES_SIZE) {
"Response size too large: ${buffer.size} bytes \n<-- END HTTP"
} else {
var gzippedLength: Long? = null var gzippedLength: Long? = null
if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) { if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) {
gzippedLength = buffer.size gzippedLength = buffer.size
@ -158,6 +169,7 @@ class NetworkLogsSaveInterceptor(
"$json\n$end" "$json\n$end"
} }
} }
}
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
@ -166,12 +178,16 @@ class NetworkLogsSaveInterceptor(
saveLogMessage( saveLogMessage(
"<-- ${response.code}", "<-- ${response.code}",
spaceBeforeResponseMessage, spaceBeforeResponseMessage,
response.message, " ${response.request.url.maskSensitiveInfo()} (${tookMs}ms)\n",
" ${response.request.url} (${tookMs}ms)\n",
message, message,
) )
} }
private fun HttpUrl.maskSensitiveInfo(): String {
val url = toString()
return sensitiveUrlMasker?.mask(url) ?: url
}
private fun bodyHasUnknownEncoding(headers: Headers): Boolean { private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
val contentEncoding = headers["Content-Encoding"] ?: return false val contentEncoding = headers["Content-Encoding"] ?: return false
return !contentEncoding.equals("identity", ignoreCase = true) && return !contentEncoding.equals("identity", ignoreCase = true) &&
@ -231,6 +247,9 @@ class NetworkLogsSaveInterceptor(
} }
private companion object { private companion object {
const val WRITE_LOG_THRESHOLD_BYTES_SIZE = 2_048_000L
/** /**
* List of URLs (host + path) for which logging is restricted * List of URLs (host + path) for which logging is restricted
*/ */

View file

@ -0,0 +1,107 @@
package com.tangem.datasource.local.logs
import com.google.common.truth.Truth
import com.tangem.datasource.local.logs.SensitiveUrlMasker.Companion.MASKED_VALUE
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class SensitiveUrlMaskerTest {
@ParameterizedTest
@ProvideTestModels
fun mask(model: TestModel) {
// Arrange
val masker = SensitiveUrlMasker(model.sensitiveValues)
// Act
val actual = masker.mask(model.input)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
@Test
fun `mask returns url unchanged when no sensitive values provided`() {
// Arrange
val masker = SensitiveUrlMasker(emptyList())
val url = "https://api.tangem.com/v1/cards/abc123"
// Act
val actual = masker.mask(url)
// Assert
Truth.assertThat(actual).isEqualTo(url)
}
@Test
fun `constructor deduplicates input values`() {
// Arrange — same secret repeated; if no dedup, replace would be invoked twice
// (idempotent on already-masked string, but we assert behavior is identical
// to a single-value masker as a smoke-check)
val withDuplicates = SensitiveUrlMasker(listOf("secret123", "secret123", "secret123"))
val withSingle = SensitiveUrlMasker(listOf("secret123"))
val url = "https://api.tangem.com/?key=secret123"
// Act
val withDup = withDuplicates.mask(url)
val withSingleResult = withSingle.mask(url)
// Assert
Truth.assertThat(withDup).isEqualTo(withSingleResult)
Truth.assertThat(withDup).isEqualTo("https://api.tangem.com/?key=$MASKED_VALUE")
}
private fun provideTestModels() = listOf(
TestModel(
input = "https://api.tangem.com/?key=secret123",
sensitiveValues = listOf("secret123"),
expected = "https://api.tangem.com/?key=$MASKED_VALUE",
),
TestModel(
input = "https://api.tangem.com/?a=alpha&b=beta",
sensitiveValues = listOf("alpha", "beta"),
expected = "https://api.tangem.com/?a=$MASKED_VALUE&b=$MASKED_VALUE",
),
TestModel(
input = "https://api.tangem.com/?key=SECRET123",
sensitiveValues = listOf("secret123"),
expected = "https://api.tangem.com/?key=$MASKED_VALUE",
),
TestModel(
input = "https://api.tangem.com/v1/balance",
sensitiveValues = listOf("notInUrl"),
expected = "https://api.tangem.com/v1/balance",
),
TestModel(
input = "https://api.tangem.com/?key=secret123&other=secret123",
sensitiveValues = listOf("secret123"),
expected = "https://api.tangem.com/?key=$MASKED_VALUE&other=$MASKED_VALUE",
),
TestModel(
input = "https://api.tangem.com/v1/cards",
sensitiveValues = emptyList(),
expected = "https://api.tangem.com/v1/cards",
),
// Regression: when one value is a prefix of another, the longer one must be masked first
// regardless of input order, otherwise the suffix leaks (e.g. "my-node-prod" -> "******-prod").
TestModel(
input = "https://my-node-prod.example.com/v1",
sensitiveValues = listOf("my-node", "my-node-prod"),
expected = "https://$MASKED_VALUE.example.com/v1",
),
TestModel(
input = "https://my-node-prod.example.com/v1",
sensitiveValues = listOf("my-node-prod", "my-node"),
expected = "https://$MASKED_VALUE.example.com/v1",
),
)
data class TestModel(
val input: String,
val sensitiveValues: List<String>,
val expected: String,
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -664,6 +664,11 @@
<string name="feedback_subject_support_tangem">Tangem feedback</string> <string name="feedback_subject_support_tangem">Tangem feedback</string>
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string> <string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
<string name="feedback_token_description_error">Coin description error</string> <string name="feedback_token_description_error">Coin description error</string>
<string name="force_update_banner_message">Update the application to the latest version to ensure proper functionality</string>
<string name="force_update_banner_title">Update Needed</string>
<string name="force_update_button">Update</string>
<string name="force_update_warning_message">Please update the application to the latest version to ensure proper functionality.</string>
<string name="force_update_warning_title">Update Required</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Not enough funds</string> <string name="gasless_not_enough_funds_to_cover_token_fee">Not enough funds</string>
<string name="gasless_transaction_fee">Transaction fee</string> <string name="gasless_transaction_fee">Transaction fee</string>
<string name="generic_error">An error occurred</string> <string name="generic_error">An error occurred</string>
@ -789,8 +794,8 @@
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana</string> <string name="koinos_mana_level_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="koinos_mana_level_title">Mana level</string>
<string name="main_add_and_manage_tokens">Add &amp; Manage</string> <string name="main_add_and_manage_tokens">Add &amp; Manage</string>
<string name="main_add_funds_promo_description">Deposit crypto or buy with card to get started</string> <string name="main_add_funds_promo_description">Buy or receive crypto to start using your wallet.</string>
<string name="main_add_funds_promo_title">Add funds to start earning and trading</string> <string name="main_add_funds_promo_title">Get your first crypto</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string> <string name="main_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_manage_tokens">Manage tokens</string>
<string name="main_qr_scan_hint">Scan QR code to send funds or connect to an app</string> <string name="main_qr_scan_hint">Scan QR code to send funds or connect to an app</string>
@ -1672,7 +1677,7 @@
<string name="tangem_pay_card_details_unable_to_rename_card_title">Unable to rename card</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_frozen">Card frozen</string>
<string name="tangem_pay_card_payment">Card payment</string> <string name="tangem_pay_card_payment">Card payment</string>
<string name="tangem_pay_close_card_popup_description">It will disappear from payment account</string> <string name="tangem_pay_close_card_popup_description">It will disappear from the app</string>
<string name="tangem_pay_close_card_popup_primary_button_title">Close card</string> <string name="tangem_pay_close_card_popup_primary_button_title">Close card</string>
<string name="tangem_pay_close_card_popup_secondary_button_title">Go back</string> <string name="tangem_pay_close_card_popup_secondary_button_title">Go back</string>
<string name="tangem_pay_close_card_popup_title">Close your card?</string> <string name="tangem_pay_close_card_popup_title">Close your card?</string>
@ -1693,6 +1698,7 @@
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string> <string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Other</string> <string name="tangem_pay_other">Other</string>
<string name="tangem_pay_pin_code_title">PIN-code</string> <string name="tangem_pay_pin_code_title">PIN-code</string>
<string name="tangem_pay_purchase">Purchase</string>
<string name="tangem_pay_rooted_device_subtitle">Unable to use on rooted devices</string> <string name="tangem_pay_rooted_device_subtitle">Unable to use on rooted devices</string>
<string name="tangem_pay_status_completed">Completed</string> <string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string> <string name="tangem_pay_status_declined">Declined</string>
@ -1701,15 +1707,17 @@
<string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string> <string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string>
<string name="tangem_pay_terms_limits">Terms and fees</string> <string name="tangem_pay_terms_limits">Terms and fees</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string> <string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string> <string name="tangem_pay_transaction_details_category">Category</string>
<string name="tangem_pay_transaction_details_mcc">MCC</string>
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged in accordance with the service tariffs</string>
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string> <string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string> <string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string> <string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string> <string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string> <string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
<string name="tangem_pay_withdrawal">Withdrawal</string> <string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_account_deactivated_message_subtitle">This was done due to regulatory requirements. Anyway withdrawals are still available.</string> <string name="tangempay_account_deactivated_message_subtitle">For questions about account, data or transaction history, please contact support</string>
<string name="tangempay_account_deactivated_message_title">Your card was deactivated</string> <string name="tangempay_account_deactivated_message_title">Your account has been closed</string>
<string name="tangempay_account_unable_to_use_rooted">Unable to use on rooted device</string> <string name="tangempay_account_unable_to_use_rooted">Unable to use on rooted device</string>
<string name="tangempay_available_balance">Available balance</string> <string name="tangempay_available_balance">Available balance</string>
<string name="tangempay_cancel_kyc">Hide KYC from main screen</string> <string name="tangempay_cancel_kyc">Hide KYC from main screen</string>
@ -1819,6 +1827,24 @@
<string name="tangempay_kyc_rejected_description_span">your profile.</string> <string name="tangempay_kyc_rejected_description_span">your profile.</string>
<string name="tangempay_maximum_cards_issued_description">You can have up to 3 cards. Delete one to add a new card.</string> <string name="tangempay_maximum_cards_issued_description">You can have up to 3 cards. Delete one to add a new card.</string>
<string name="tangempay_maximum_cards_issued_title">Maximum Cards Issued</string> <string name="tangempay_maximum_cards_issued_title">Maximum Cards Issued</string>
<string name="tangempay_newonboard_Q1_body">Yes to operate a regulated Visa card, identity verification is mandatory. KYC is handled by Sumsub (compliance partner).</string>
<string name="tangempay_newonboard_Q1_title">Do I have to share my docs?</string>
<string name="tangempay_newonboard_Q2_body">No. KYC applies only to the Tangem Pay account. Your Tangem Wallet itself remains a separate, self-custodial, non-KYC environment.</string>
<string name="tangempay_newonboard_Q2_title">Does the KYC associate with my wallet?</string>
<string name="tangempay_newonboard_Q3_body">Sumsub a globally regulated KYC provider, trusted by 4,000+ financial institutions verifies your identity and securely stores the results under ISO 27001 and SOC 2 standards.</string>
<string name="tangempay_newonboard_Q3_title">Who stores my personal data and how is it protected?</string>
<string name="tangempay_newonboard_Q4_body">Card balance nominated in USDC on Polygon, but you can use any asset (USDT, SOL, ETH, BTC, XRP etc.) to fund it using Tangem\'s convenient built-in swap mechanisms.</string>
<string name="tangempay_newonboard_Q4_title">What crypto can I spend?</string>
<string name="tangempay_newonboard_body">Spend crypto anywhere — no banks, no middlemen, no exchanges. The power of self-custody meets everyday payments.</string>
<string name="tangempay_newonboard_bottomleft_body">Buy online and via Apple Pay</string>
<string name="tangempay_newonboard_bottomleft_title">Accepted worldwide</string>
<string name="tangempay_newonboard_bottomright_body">Use anywhere in the world</string>
<string name="tangempay_newonboard_bottomright_title">FX fee is just 1%</string>
<string name="tangempay_newonboard_title">Get your Tangem Pay Card</string>
<string name="tangempay_newonboard_topleft_body">Pay what you see</string>
<string name="tangempay_newonboard_topleft_title">No purchase fees, \n1 USDC = 1 USD</string>
<string name="tangempay_newonboard_topright_body">No surprises</string>
<string name="tangempay_newonboard_topright_title">$0 monthly fee\n$0 topup fee</string>
<string name="tangempay_onboarding_banner_description">Get your free Tangem Visa virtual card</string> <string name="tangempay_onboarding_banner_description">Get your free Tangem Visa virtual card</string>
<string name="tangempay_onboarding_banner_title">Use USDC for everyday payments</string> <string name="tangempay_onboarding_banner_title">Use USDC for everyday payments</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string> <string name="tangempay_onboarding_get_card_button_text">Get card</string>
@ -1830,7 +1856,7 @@
<string name="tangempay_onboarding_security_title">Unrivaled privacy</string> <string name="tangempay_onboarding_security_title">Unrivaled privacy</string>
<string name="tangempay_onboarding_setup_wallet_description">And link a payment card to it</string> <string name="tangempay_onboarding_setup_wallet_description">And link a payment card to it</string>
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string> <string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
<string name="tangempay_onboarding_title">Get your free Tangem Pay Card in minutes</string> <string name="tangempay_onboarding_title">Get your Tangem Pay Card in minutes</string>
<string name="tangempay_pay_support">Pay Support</string> <string name="tangempay_pay_support">Pay Support</string>
<string name="tangempay_payment_account">Payment account</string> <string name="tangempay_payment_account">Payment account</string>
<string name="tangempay_payment_account_sync_needed">Payment account session expired</string> <string name="tangempay_payment_account_sync_needed">Payment account session expired</string>
@ -1847,16 +1873,16 @@
<string name="tangempay_reissue_card_title">Replace your card?</string> <string name="tangempay_reissue_card_title">Replace your card?</string>
<string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string> <string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string>
<string name="tangempay_service_unavailable_title">Service temporarily unavailable</string> <string name="tangempay_service_unavailable_title">Service temporarily unavailable</string>
<string name="tangempay_service_unreachable_try_later">Unable to display details. However, card payments are still working.</string> <string name="tangempay_service_unreachable_try_later">The service is currently unreachable. Please try again later.</string>
<string name="tangempay_set_pin_code">Set \nPIN code</string> <string name="tangempay_set_pin_code">Set \nPIN code</string>
<string name="tangempay_status_deactivated">Card deactivated</string> <string name="tangempay_status_deactivated">Account closed</string>
<string name="tangempay_status_replacing">Replacing your card</string> <string name="tangempay_status_replacing">Replacing your card</string>
<string name="tangempay_sync_needed">Use your card or ring to renew session</string> <string name="tangempay_sync_needed">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_body">Use your card or ring to renew session</string> <string name="tangempay_sync_needed_body">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_button">Renew session</string> <string name="tangempay_sync_needed_button">Renew session</string>
<string name="tangempay_sync_needed_title">Payment account session expired</string> <string name="tangempay_sync_needed_title">Payment account session expired</string>
<string name="tangempay_tangem_visa_card">Use USDC for everyday payments</string> <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_temporarily_unavailable">Tangem Pay is temporarily unavailable</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Send USDC Polygon to your accounts address </string> <string name="tangempay_topup_receive_body">Send USDC Polygon to your accounts address </string>
<string name="tangempay_topup_receive_title">From another wallet or exchange</string> <string name="tangempay_topup_receive_title">From another wallet or exchange</string>

View file

@ -1,7 +1,7 @@
package com.tangem.core.ui.components.provider package com.tangem.core.ui.components.provider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.key import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.domain.express.models.ProviderFilterType import com.tangem.domain.express.models.ProviderFilterType
@ -20,7 +20,8 @@ fun ProviderTypeFilterPicker(
onFilterSelect: (ProviderFilterType) -> Unit, onFilterSelect: (ProviderFilterType) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val segments = availableFilters.map { filter -> val segments = remember(availableFilters) {
availableFilters.map { filter ->
TangemSegmentUM( TangemSegmentUM(
id = filter.name, id = filter.name,
title = when (filter) { title = when (filter) {
@ -30,11 +31,11 @@ fun ProviderTypeFilterPicker(
}, },
) )
}.toImmutableList() }.toImmutableList()
val selectedSegment = segments.firstOrNull { it.id == selectedFilter.name } }
val selectedSegment = remember(segments, selectedFilter) {
segments.firstOrNull { it.id == selectedFilter.name }
}
TangemThemeRedesign { TangemThemeRedesign {
// key() forces recomposition when selectedFilter changes to re-seed initialSelectedItem,
// because TangemSegmentedPicker owns its selection state internally via remember.
key(selectedFilter) {
TangemSegmentedPicker( TangemSegmentedPicker(
items = segments, items = segments,
initialSelectedItem = selectedSegment, initialSelectedItem = selectedSegment,
@ -47,4 +48,3 @@ fun ProviderTypeFilterPicker(
) )
} }
} }
}

View file

@ -1,21 +1,14 @@
package com.tangem.core.ui.ds2.surface package com.tangem.core.ui.ds2.surface
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.*
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.LocalRippleConfiguration
import androidx.compose.material3.RippleConfiguration import androidx.compose.material3.RippleConfiguration
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -94,6 +87,7 @@ fun TangemSurface(
onClick = onClick!!, onClick = onClick!!,
) )
}, },
contentAlignment = Alignment.Center,
) { ) {
content() content()
} }

View file

@ -464,6 +464,10 @@ val LocalRedesignEnabled = staticCompositionLocalOf<Boolean> {
false false
} }
val LocalVisaRedesignEnabled = staticCompositionLocalOf<Boolean> {
false
}
val LocalPowerSavingState = compositionLocalOf<PowerSavingState> { val LocalPowerSavingState = compositionLocalOf<PowerSavingState> {
error("No PowerSavingState provided") error("No PowerSavingState provided")
} }

View file

@ -1,27 +0,0 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<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="M15.75,9.25C16.164,9.25 16.5,9.586 16.5,10C16.5,13.59 13.59,16.5 10,16.5C7.986,16.5 6.19,15.581 5,14.144V14.791C5,15.205 4.664,15.541 4.25,15.541C3.836,15.541 3.5,15.205 3.5,14.791V12.396C3.5,11.981 3.836,11.646 4.25,11.646H6.646C7.06,11.646 7.395,11.981 7.396,12.396C7.396,12.81 7.06,13.146 6.646,13.146H6.123C7.04,14.276 8.434,15 10,15C12.762,15 15,12.762 15,10C15,9.586 15.336,9.25 15.75,9.25Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M10,3.5C12.014,3.5 13.81,4.419 15,5.855V5.208C15,4.794 15.336,4.458 15.75,4.458C16.164,4.458 16.5,4.794 16.5,5.208V7.604C16.5,8.018 16.164,8.354 15.75,8.354H13.354C12.94,8.353 12.604,8.018 12.604,7.604C12.605,7.19 12.941,6.854 13.354,6.854H13.876C12.96,5.723 11.565,5 10,5C7.238,5 5,7.238 5,10C5,10.414 4.664,10.75 4.25,10.75C3.836,10.75 3.5,10.414 3.5,10C3.5,6.41 6.41,3.5 10,3.5Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -1,6 +1,7 @@
plugins { plugins {
alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.kapt) alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
id("configuration") id("configuration")
} }
@ -15,12 +16,13 @@ dependencies {
kapt(deps.hilt.kapt) kapt(deps.hilt.kapt)
// endregion // endregion
// region Coroutines // region Kotlin
implementation(deps.kotlin.coroutines) api(deps.kotlin.coroutines)
api(deps.kotlin.serialization)
// endregion // endregion
// region Time dependencies // region Time dependencies
implementation(deps.jodatime) api(deps.jodatime)
// endregion // endregion
testImplementation(deps.test.coroutine) testImplementation(deps.test.coroutine)

View file

@ -0,0 +1,22 @@
package com.tangem.utils
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
/**
* Extracts all string primitive values from a [JsonElement] tree (recursively into
* objects and arrays). Non-string primitives are ignored.
*/
object JsonStringValuesExtractor {
fun extract(json: JsonElement): List<String> = json.extractStringValues()
private fun JsonElement.extractStringValues(): List<String> = when (this) {
is JsonPrimitive -> if (isString) listOfNotNull(contentOrNull) else emptyList()
is JsonObject -> values.flatMap { it.extractStringValues() }
is JsonArray -> flatMap { it.extractStringValues() }
}
}

View file

@ -0,0 +1,171 @@
package com.tangem.utils
import com.google.common.truth.Truth
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class JsonStringValuesExtractorTest {
@Test
fun `extract returns single value for string primitive`() {
// Arrange
val json = JsonPrimitive("hello")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("hello")
}
@Test
fun `extract returns empty for numeric primitive`() {
// Arrange
val json = JsonPrimitive(42)
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `extract returns empty for boolean primitive`() {
// Arrange
val json = JsonPrimitive(true)
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `extract returns empty for json null`() {
// Act
val actual = JsonStringValuesExtractor.extract(JsonNull)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `extract returns all string values from flat object`() {
// Arrange
val json = Json.parseToJsonElement(
"""{"apiKey":"abc","secret":"xyz","count":42,"enabled":true}""",
)
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("abc", "xyz")
}
@Test
fun `extract returns all string values from flat array`() {
// Arrange
val json = Json.parseToJsonElement("""["one","two",3,true,null]""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("one", "two").inOrder()
}
@Test
fun `extract recurses into nested objects`() {
// Arrange
val json = Json.parseToJsonElement(
"""{"outer":{"inner":{"key":"deep"}},"top":"shallow"}""",
)
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("deep", "shallow")
}
@Test
fun `extract recurses into nested arrays`() {
// Arrange
val json = Json.parseToJsonElement("""[["a","b"],["c",["d"]]]""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("a", "b", "c", "d").inOrder()
}
@Test
fun `extract handles mixed nested objects and arrays`() {
// Arrange
val json = Json.parseToJsonElement(
"""{"keys":["k1","k2"],"nested":{"items":[{"name":"x"},{"name":"y"}]}}""",
)
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("k1", "k2", "x", "y")
}
@Test
fun `extract returns empty for empty object`() {
// Arrange
val json = Json.parseToJsonElement("""{}""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `extract returns empty for empty array`() {
// Arrange
val json = Json.parseToJsonElement("""[]""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `extract preserves duplicate values`() {
// Arrange — extractor does NOT dedupe; that's the caller's concern
val json = Json.parseToJsonElement("""{"a":"same","b":"same","c":"other"}""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert
Truth.assertThat(actual).containsExactly("same", "same", "other")
}
@Test
fun `extract returns empty string when string primitive is empty`() {
// Arrange
val json = Json.parseToJsonElement("""{"a":"","b":"x"}""")
// Act
val actual = JsonStringValuesExtractor.extract(json)
// Assert — extractor returns "" too; filtering is caller's job
Truth.assertThat(actual).containsExactly("", "x")
}
}

View file

@ -26,6 +26,7 @@ dependencies {
// region Project - Domain // region Project - Domain
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.common)
implementation(projects.domain.dynamicAddresses) implementation(projects.domain.dynamicAddresses)
implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.dynamicAddresses.models)
implementation(projects.domain.models) implementation(projects.domain.models)

View file

@ -1,5 +1,7 @@
package com.tangem.data.dynamicaddresses package com.tangem.data.dynamicaddresses
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncOrNull
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
@ -7,6 +9,7 @@ import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject import javax.inject.Inject
@ -21,11 +24,22 @@ class DynamicAddressesInitializer @Inject constructor(
private val dynamicAddressesRepository: DynamicAddressesRepository, private val dynamicAddressesRepository: DynamicAddressesRepository,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
private val getDerivedXpubUseCase: GetDerivedXpubUseCase, private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
) { ) {
suspend fun getXpubs(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, String> { suspend fun getXpubs(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, String> {
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap() if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap()
/*
* Dynamic addresses rely on the server-side wallet accounts list, which is populated only for
* multi-currency wallets. Single-currency wallets (Note, s2c, etc.) never populate it, so
* DynamicAddressesRepository.getStatus() backed by WalletAccountsFetcher.get() would never
* emit and firstOrNull() below would suspend forever, hanging the whole balance fetch and leaving
* the currency stuck in Loading. Skip such wallets entirely. ([REDACTED_TASK_KEY])
*/
val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId)
if (userWallet == null || !userWallet.isMultiCurrency) return emptyMap()
val result = mutableMapOf<Network, String>() val result = mutableMapOf<Network, String>()
for (network in networks) { for (network in networks) {
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue

View file

@ -1,55 +0,0 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
@Deprecated("Use TangemPayCurrencyFactory instead")
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val errorConverter: TangemPayErrorConverter,
) : TangemPayCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
NetworkFactory(excludedBlockchains)
}
override fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency> {
return catch {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
val blockchain = requireNotNull(chain.blockchain)
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID),
name = TangemPayCurrencyFactory.TOKEN_NAME,
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
)
}.mapLeft { exception ->
TangemLogger.withTag(TAG).e("Error", exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.data.pay.converter package com.tangem.data.pay.converter
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.account.CardDisplayName
@ -11,7 +10,9 @@ import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardLimit import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -43,6 +44,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
fiatBalance = value.fiatBalance.toDM(), fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(),
availableForWithdrawal = value.availableForWithdrawal, availableForWithdrawal = value.availableForWithdrawal,
fiatRate = value.fiatRate,
cards = value.cards.map { card -> cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard( PaymentAccountStatusValueDM.TangemPayCard(
id = card.id, id = card.id,
@ -52,7 +54,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
adminDailyLimit = card.limit?.adminCardLimit?.amount, adminDailyLimit = card.limit?.adminCardLimit?.amount,
frozenState = card.frozenState.toString(), frozenState = card.frozenState.toString(),
lastDigits = card.lastDigits, lastDigits = card.lastDigits,
isReissuing = card.isReissuing, state = card.state.toString(),
) )
}, },
) )
@ -61,6 +63,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
) )
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty() is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount( is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
fiatRate = value.fiatRate,
fiatBalance = value.fiatBalance.toDM(), fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(),
) )
@ -93,6 +96,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
cryptoBalance = value.cryptoBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal, availableForWithdrawal = value.availableForWithdrawal,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate,
cards = value.cards.map { card -> cards = value.cards.map { card ->
TangemPayCard( TangemPayCard(
id = card.id, id = card.id,
@ -108,7 +112,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
), ),
frozenState = TangemPayCardFrozenState.fromString(card.frozenState), frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
lastDigits = card.lastDigits, lastDigits = card.lastDigits,
isReissuing = card.isReissuing, state = TangemPayCardState.fromString(card.state),
) )
}, },
) )
@ -122,6 +126,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
fiatBalance = value.fiatBalance.toDomain(), fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate,
) )
null -> PaymentAccountStatusValue.Error.Unavailable null -> PaymentAccountStatusValue.Error.Unavailable
} }

View file

@ -5,9 +5,9 @@ import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.DefaultTangemPayEligibilityManager
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
import com.tangem.data.pay.entity.DefaultTangemPayCurrencyFactory
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
import com.tangem.data.pay.repository.* import com.tangem.data.pay.repository.*
@ -21,19 +21,13 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.pay.usecase.*
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
@ -75,9 +69,11 @@ internal interface TangemPayDataModule {
@Binds @Binds
@Singleton @Singleton
fun bindTangemPayCryptoCurrencyFactory( fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository
factory: DefaultTangemPayCryptoCurrencyFactory,
): TangemPayCryptoCurrencyFactory @Binds
@Singleton
fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory
@Binds @Binds
@Singleton @Singleton
@ -220,5 +216,20 @@ internal interface TangemPayDataModule {
paymentAccountStatusFetcher = paymentAccountStatusFetcher, paymentAccountStatusFetcher = paymentAccountStatusFetcher,
) )
} }
@Provides
fun provideCloseTangemPayCardUseCase(
closeCardRepository: TangemPayCloseCardRepository,
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
appCoroutineScope: AppCoroutineScope,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): CloseTangemPayCardUseCase {
return CloseTangemPayCardUseCase(
closeCardRepository = closeCardRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = appCoroutineScope,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
}
} }
} }

View file

@ -8,20 +8,21 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.requireUserWalletsSync import com.tangem.domain.common.wallets.requireUserWalletsSync
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
internal class TangemPayCurrencyFactory @Inject constructor( internal class DefaultTangemPayCurrencyFactory @Inject constructor(
excludedBlockchains: ExcludedBlockchains, excludedBlockchains: ExcludedBlockchains,
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val networkFactory: NetworkFactory, private val networkFactory: NetworkFactory,
) { ) : TangemPayCurrencyFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains) CryptoCurrencyFactory(excludedBlockchains)
} }
fun create(userWalletId: UserWalletId): CryptoCurrency.Token { override fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
val userWallet = userWalletsListRepository.requireUserWalletsSync() val userWallet = userWalletsListRepository.requireUserWalletsSync()
.firstOrNull { it.walletId == userWalletId } .firstOrNull { it.walletId == userWalletId }
?: error("User wallet with id $userWalletId not found") ?: error("User wallet with id $userWalletId not found")
@ -32,18 +33,11 @@ internal class TangemPayCurrencyFactory @Inject constructor(
) )
return cryptoCurrencyFactory.createToken( return cryptoCurrencyFactory.createToken(
network = requireNotNull(network), network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID), rawId = TangemPayCurrencyFactory.TOKEN_ID,
name = TOKEN_NAME, name = TangemPayCurrencyFactory.TOKEN_NAME,
symbol = TOKEN_NAME, symbol = TangemPayCurrencyFactory.TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS, contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS, decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
) )
} }
companion object {
internal const val TOKEN_ID = "usd-coin"
internal const val TOKEN_NAME = "USDC"
internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
internal const val TOKEN_DECIMALS = 6
}
} }

View file

@ -1,7 +1,6 @@
package com.tangem.data.pay.flow package com.tangem.data.pay.flow
import arrow.core.Either import arrow.core.Either
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
@ -11,7 +10,9 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
@ -21,9 +22,14 @@ import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.pay.model.isFinalStatus
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -31,12 +37,13 @@ import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher" private const val TAG = "PaymentAccountStatusFetcher"
@Suppress("LongParameterList") @Suppress("LongParameterList", "LargeClass")
internal class DefaultPaymentAccountStatusFetcher @Inject constructor( internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
@ -46,6 +53,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val eligibilityManager: TangemPayEligibilityManager, private val eligibilityManager: TangemPayEligibilityManager,
private val reissueCardRepository: TangemPayReissueCardRepository, private val reissueCardRepository: TangemPayReissueCardRepository,
private val singleQuoteSupplier: SingleQuoteStatusSupplier,
private val closeCardRepository: TangemPayCloseCardRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : PaymentAccountStatusFetcher { ) : PaymentAccountStatusFetcher {
@ -259,6 +268,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
} }
private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val quotesData = singleQuoteSupplier.getSyncOrNull(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID),
)?.value as? QuoteStatus.Data
val cardInfo = this.cardInfo val cardInfo = this.cardInfo
val productInstance = this.productInstance val productInstance = this.productInstance
@ -281,12 +293,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
fiatBalance = fiatBalance, fiatBalance = fiatBalance,
cryptoBalance = cryptoBalance, cryptoBalance = cryptoBalance,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = quotesData?.fiatRate,
) )
} }
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState( cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
userWalletId = userWalletId, userWalletId = userWalletId,
productInstance = productInstance, productInstance = productInstance,
cardInfo = cardInfo, cardInfo = cardInfo,
fiatRate = quotesData?.fiatRate,
customerId = requireNotNull(customerId) { "CustomerId must not be null" }, customerId = requireNotNull(customerId) { "CustomerId must not be null" },
) )
else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
@ -298,17 +312,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
productInstance: CustomerInfo.ProductInstance, productInstance: CustomerInfo.ProductInstance,
cardInfo: CustomerInfo.CardInfo, cardInfo: CustomerInfo.CardInfo,
customerId: String, customerId: String,
fiatRate: BigDecimal?,
): PaymentAccountStatusValue { ): PaymentAccountStatusValue {
val reissueOrder = reissueCardRepository.getReissueOrderInfo( val cardId = productInstance.cardId
userWalletId = userWalletId, val cardState = getCardState(cardId, userWalletId)
cardId = productInstance.cardId, val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(cardId)
).getOrNull()
val isReissuing = reissueOrder != null &&
reissueOrder.orderStatus != OrderStatus.CANCELED &&
reissueOrder.orderStatus != OrderStatus.COMPLETED
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(productInstance.cardId)
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId) val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return PaymentAccountStatusValue.Loaded( return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
@ -319,9 +327,10 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
cryptoBalance = cardInfo.cryptoBalance, cryptoBalance = cardInfo.cryptoBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal, availableForWithdrawal = cardInfo.availableForWithdrawal,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = fiatRate,
cards = listOf( cards = listOf(
TangemPayCard( TangemPayCard(
id = productInstance.cardId, id = cardId,
hasPinCode = cardInfo.isPinSet, hasPinCode = cardInfo.isPinSet,
displayName = productInstance.displayName, displayName = productInstance.displayName,
limit = TangemPayCardLimitData( limit = TangemPayCardLimitData(
@ -334,12 +343,35 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
productInstance.frozenState productInstance.frozenState
}, },
lastDigits = cardInfo.lastFourDigits, lastDigits = cardInfo.lastFourDigits,
isReissuing = isReissuing, state = cardState,
), ),
), ),
) )
} }
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()
return if (closingOrderId != null) {
val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull()
if (order != null && order.orderStatus.isFinalStatus) {
closeCardRepository.setCloseOrderId(cardId, null)
TangemPayCardState.Active
} else {
TangemPayCardState.Closing
}
} else if (reissueOrderId != null) {
val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull()
if (order != null && order.orderStatus.isFinalStatus) {
TangemPayCardState.Active
} else {
TangemPayCardState.Reissuing
}
} else {
TangemPayCardState.Active
}
}
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
return when (this) { return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced

View file

@ -0,0 +1,52 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CloseCardRequest
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching
import javax.inject.Inject
internal class DefaultCloseCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayCloseCardStore: TangemPayCloseCardStore,
) : TangemPayCloseCardRepository {
override suspend fun closeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<VisaApiError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.closeCard(
authHeader = authHeader,
body = CloseCardRequest(cardId = cardId),
)
}.bind()
TangemPayOrderInfo(
orderId = response.result.orderId,
orderStatus = OrderStatusConverter.convert(response.result.status),
)
}
override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit> =
runSuspendCatching {
tangemPayCloseCardStore.setCloseOrderId(cardId, orderId)
}.fold(
onSuccess = { Unit.right() },
onFailure = { Either.Left(VisaApiError.Unspecified) },
)
override suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> =
either {
runSuspendCatching { tangemPayCloseCardStore.getOrderId(cardId) }.getOrNull()
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.runSuspendCatching
@ -21,7 +20,6 @@ internal class DefaultReissueCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi, private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer, private val requestHelper: TangemPayRequestPerformer,
private val tangemPayReissueCardStore: TangemPayReissueCardStore, private val tangemPayReissueCardStore: TangemPayReissueCardStore,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : TangemPayReissueCardRepository { ) : TangemPayReissueCardRepository {
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> = override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
@ -74,17 +72,11 @@ internal class DefaultReissueCardRepository @Inject constructor(
onFailure = { Either.Left(VisaApiError.Unspecified) }, onFailure = { Either.Left(VisaApiError.Unspecified) },
) )
override suspend fun getReissueOrderInfo( override suspend fun getReissueOrderId(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cardId: String, cardId: String,
): Either<UniversalError, TangemPayOrderInfo?> = either { ): Either<UniversalError, String?> = either {
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull() runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
if (orderId == null) {
return null.right()
}
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
} }
private companion object { private companion object {

View file

@ -1,12 +1,12 @@
package com.tangem.data.pay.converter package com.tangem.data.pay.converter
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
@ -70,6 +70,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
), ),
cryptoBalance = cryptoBalance(), cryptoBalance = cryptoBalance(),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = BigDecimal("1.05"),
) )
// WHEN // WHEN
@ -80,6 +81,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount
assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100")) assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100"))
assertThat(dm.fiatBalance.currency).isEqualTo("USD") assertThat(dm.fiatBalance.currency).isEqualTo("USD")
assertThat(dm.fiatRate).isEqualTo(BigDecimal("1.05"))
} }
@Test @Test
@ -144,6 +146,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
currency = "EUR", currency = "EUR",
), ),
cryptoBalance = cryptoBalanceDM(), cryptoBalance = cryptoBalanceDM(),
fiatRate = BigDecimal("0.92"),
) )
// WHEN // WHEN
@ -155,6 +158,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
assertThat(deactivated.source).isEqualTo(StatusSource.CACHE) assertThat(deactivated.source).isEqualTo(StatusSource.CACHE)
assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200")) assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200"))
assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR") assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR")
assertThat(deactivated.fiatRate).isEqualTo(BigDecimal("0.92"))
} }
@Test @Test

View file

@ -16,43 +16,26 @@ internal object YieldBoostStatusConverter {
private const val REASON_CLOSED = "closed" private const val REASON_CLOSED = "closed"
fun convert(dto: YieldBoostStatusResponse): YieldBoostStatus = when (dto.promoEnrollmentStatus.lowercase()) { fun convert(dto: YieldBoostStatusResponse): YieldBoostStatus = when (dto.promoEnrollmentStatus.lowercase()) {
STATUS_ACTIVE -> dto.toActive() ?: YieldBoostStatus.NotStarted STATUS_ACTIVE, STATUS_COMPLETED -> dto.toEnrolled()
STATUS_COMPLETED -> dto.toCompleted() ?: YieldBoostStatus.NotStarted
STATUS_DISQUALIFIED -> YieldBoostStatus.Disqualified(reason = dto.disqualificationReason.toReason()) STATUS_DISQUALIFIED -> YieldBoostStatus.Disqualified(reason = dto.disqualificationReason.toReason())
STATUS_NOT_STARTED -> YieldBoostStatus.NotStarted STATUS_NOT_STARTED -> YieldBoostStatus.NotStarted
else -> YieldBoostStatus.NotStarted // forward-compat: unknown status → treat as NotStarted else -> YieldBoostStatus.NotStarted // forward-compat: unknown status → treat as NotStarted
} }
/** Backend `"active"` → [YieldBoostStatus.Active]. Returns `null` if mandatory dates can't be parsed. */ /**
private fun YieldBoostStatusResponse.toActive(): YieldBoostStatus.Active? { * Backend `"active"` / `"completed"` [YieldBoostStatus.Enrolled].
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null *
val qualificationEnd = * An unparseable / missing `qualificationEndDate` is kept as `null` (block hidden) never downgraded to
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null * [YieldBoostStatus.NotStarted], which would re-prompt an already-enrolled user to join.
return YieldBoostStatus.Active( */
private fun YieldBoostStatusResponse.toEnrolled(): YieldBoostStatus.Enrolled = YieldBoostStatus.Enrolled(
tokenName = tokenName.orEmpty(), tokenName = tokenName.orEmpty(),
networkId = networkId.orEmpty(), networkId = networkId.orEmpty(),
moduleAddress = moduleAddress.orEmpty(), moduleAddress = moduleAddress.orEmpty(),
userAddress = userAddress.orEmpty(), userAddress = userAddress.orEmpty(),
contractAddress = contractAddress.orEmpty(), contractAddress = contractAddress.orEmpty(),
activationDate = activation, qualificationEndDate = qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() },
qualificationEndDate = qualificationEnd,
) )
}
private fun YieldBoostStatusResponse.toCompleted(): YieldBoostStatus.Completed? {
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
val qualificationEnd =
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
return YieldBoostStatus.Completed(
tokenName = tokenName.orEmpty(),
networkId = networkId.orEmpty(),
moduleAddress = moduleAddress.orEmpty(),
userAddress = userAddress.orEmpty(),
contractAddress = contractAddress.orEmpty(),
activationDate = activation,
qualificationEndDate = qualificationEnd,
)
}
private fun String?.toReason(): YieldBoostStatus.Disqualified.Reason = when (this?.lowercase()) { private fun String?.toReason(): YieldBoostStatus.Disqualified.Reason = when (this?.lowercase()) {
REASON_FROD -> YieldBoostStatus.Disqualified.Reason.FROD REASON_FROD -> YieldBoostStatus.Disqualified.Reason.FROD

View file

@ -3,11 +3,11 @@ package com.tangem.data.yield.supply.promo.converter
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.models.YieldBoostStatus
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class YieldBoostStatusConverterTest { class YieldBoostStatusConverterTest {
private val activation = "2026-05-01T00:00:00Z"
private val qualificationEnd = "2026-06-01T00:00:00Z" private val qualificationEnd = "2026-06-01T00:00:00Z"
@Test @Test
@ -20,7 +20,7 @@ class YieldBoostStatusConverterTest {
} }
@Test @Test
fun `GIVEN active backend status with valid dates WHEN convert THEN returns Active`() { fun `GIVEN active backend status with valid date WHEN convert THEN returns Enrolled`() {
val dto = dto( val dto = dto(
promoEnrollmentStatus = "active", promoEnrollmentStatus = "active",
tokenName = "USD Coin", tokenName = "USD Coin",
@ -28,47 +28,49 @@ class YieldBoostStatusConverterTest {
moduleAddress = "0xmodule", moduleAddress = "0xmodule",
userAddress = "0xuser", userAddress = "0xuser",
contractAddress = "0xcontract", contractAddress = "0xcontract",
activationDate = activation,
qualificationEndDate = qualificationEnd, qualificationEndDate = qualificationEnd,
) )
val result = YieldBoostStatusConverter.convert(dto) val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java) assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
val active = result as YieldBoostStatus.Active val enrolled = result as YieldBoostStatus.Enrolled
assertThat(active.tokenName).isEqualTo("USD Coin") assertThat(enrolled.tokenName).isEqualTo("USD Coin")
assertThat(active.networkId).isEqualTo("ethereum") assertThat(enrolled.networkId).isEqualTo("ethereum")
assertThat(active.contractAddress).isEqualTo("0xcontract") assertThat(enrolled.contractAddress).isEqualTo("0xcontract")
assertThat(enrolled.qualificationEndDate).isEqualTo(Instant.parse(qualificationEnd))
} }
@Test @Test
fun `GIVEN active status missing activationDate WHEN convert THEN falls back to NotStarted`() { fun `GIVEN active status missing qualificationEndDate WHEN convert THEN returns Enrolled with null date`() {
val dto = dto( val dto = dto(
promoEnrollmentStatus = "active", promoEnrollmentStatus = "active",
activationDate = null, contractAddress = "0xcontract",
qualificationEndDate = qualificationEnd, qualificationEndDate = null,
) )
val result = YieldBoostStatusConverter.convert(dto) val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted) assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull()
} }
@Test @Test
fun `GIVEN active status with malformed activationDate WHEN convert THEN falls back to NotStarted`() { fun `GIVEN active status with malformed qualificationEndDate WHEN convert THEN returns Enrolled with null date`() {
val dto = dto( val dto = dto(
promoEnrollmentStatus = "active", promoEnrollmentStatus = "active",
activationDate = "not-an-iso", contractAddress = "0xcontract",
qualificationEndDate = qualificationEnd, qualificationEndDate = "not-an-iso",
) )
val result = YieldBoostStatusConverter.convert(dto) val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted) assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull()
} }
@Test @Test
fun `GIVEN completed status with valid dates WHEN convert THEN returns Completed`() { fun `GIVEN completed status with valid date WHEN convert THEN returns Enrolled`() {
val dto = dto( val dto = dto(
promoEnrollmentStatus = "completed", promoEnrollmentStatus = "completed",
tokenName = "USDT", tokenName = "USDT",
@ -76,13 +78,14 @@ class YieldBoostStatusConverterTest {
moduleAddress = "0xmodule", moduleAddress = "0xmodule",
userAddress = "0xuser", userAddress = "0xuser",
contractAddress = "0xcontract", contractAddress = "0xcontract",
activationDate = "2026-04-01T00:00:00Z",
qualificationEndDate = "2026-05-01T00:00:00Z", qualificationEndDate = "2026-05-01T00:00:00Z",
) )
val result = YieldBoostStatusConverter.convert(dto) val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Completed::class.java) assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate)
.isEqualTo(Instant.parse("2026-05-01T00:00:00Z"))
} }
@Test @Test
@ -153,13 +156,12 @@ class YieldBoostStatusConverterTest {
moduleAddress = "0xmodule", moduleAddress = "0xmodule",
userAddress = "0xuser", userAddress = "0xuser",
contractAddress = "0xcontract", contractAddress = "0xcontract",
activationDate = activation,
qualificationEndDate = qualificationEnd, qualificationEndDate = qualificationEnd,
) )
val result = YieldBoostStatusConverter.convert(dto) val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java) assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
} }
private fun dto( private fun dto(
@ -169,7 +171,6 @@ class YieldBoostStatusConverterTest {
moduleAddress: String? = null, moduleAddress: String? = null,
userAddress: String? = null, userAddress: String? = null,
contractAddress: String? = null, contractAddress: String? = null,
activationDate: String? = null,
qualificationEndDate: String? = null, qualificationEndDate: String? = null,
disqualificationReason: String? = null, disqualificationReason: String? = null,
) = YieldBoostStatusResponse( ) = YieldBoostStatusResponse(
@ -179,7 +180,6 @@ class YieldBoostStatusConverterTest {
userAddress = userAddress, userAddress = userAddress,
contractAddress = contractAddress, contractAddress = contractAddress,
promoEnrollmentStatus = promoEnrollmentStatus, promoEnrollmentStatus = promoEnrollmentStatus,
activationDate = activationDate,
qualificationEndDate = qualificationEndDate, qualificationEndDate = qualificationEndDate,
disqualificationReason = disqualificationReason, disqualificationReason = disqualificationReason,
) )

View file

@ -31,8 +31,14 @@ sealed class PaymentAccountStatusValue {
is UnderReview, is UnderReview,
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source) -> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
is Loading -> TotalFiatBalance.Loading is Loading -> TotalFiatBalance.Loading
is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) is Loaded -> {
is Deactivated -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) val rate = this.fiatRate ?: return TotalFiatBalance.Failed
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
}
is Deactivated -> {
val rate = this.fiatRate ?: return TotalFiatBalance.Failed
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
}
} }
/** /**
@ -99,6 +105,11 @@ sealed class PaymentAccountStatusValue {
* *
* @property source The source of the status information. * @property source The source of the status information.
* @property fiatBalance The fiat balance details. * @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details.
* @property cryptoCurrency The crypto currency held by the deactivated account.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`,
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
*/ */
@Serializable @Serializable
data class Deactivated( data class Deactivated(
@ -106,25 +117,15 @@ sealed class PaymentAccountStatusValue {
val fiatBalance: FiatBalance, val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance, val cryptoBalance: CryptoBalance,
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val fiatRate: SerializedBigDecimal?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded( value = buildCryptoCurrencyStatusValue(
amount = cryptoBalance.balance, amount = cryptoBalance.balance,
fiatAmount = fiatBalance.availableBalance, fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE, fiatRate = fiatRate,
priceChange = BigDecimal.ZERO, depositAddress = cryptoBalance.depositAddress,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = cryptoBalance.depositAddress,
),
),
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
), ),
) )
} }
@ -139,7 +140,11 @@ sealed class PaymentAccountStatusValue {
* @property fiatBalance The fiat balance details. * @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details. * @property cryptoBalance The crypto balance details.
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds). * @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
* @property cryptoCurrency The crypto currency held by the account.
* @property cards The list of user's cards. * @property cards The list of user's cards.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`,
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
*/ */
@Serializable @Serializable
data class Loaded( data class Loaded(
@ -152,25 +157,15 @@ sealed class PaymentAccountStatusValue {
val availableForWithdrawal: SerializedBigDecimal, val availableForWithdrawal: SerializedBigDecimal,
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val cards: List<TangemPayCard>, val cards: List<TangemPayCard>,
val fiatRate: SerializedBigDecimal?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded( value = buildCryptoCurrencyStatusValue(
amount = availableForWithdrawal, amount = availableForWithdrawal,
fiatAmount = fiatBalance.availableBalance, fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE, fiatRate = fiatRate,
priceChange = BigDecimal.ZERO, depositAddress = cryptoBalance.depositAddress,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = cryptoBalance.depositAddress,
),
),
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
), ),
) )
} }
@ -235,6 +230,44 @@ sealed class PaymentAccountStatusValue {
) )
} }
private fun buildCryptoCurrencyStatusValue(
amount: SerializedBigDecimal,
fiatAmount: SerializedBigDecimal,
fiatRate: SerializedBigDecimal?,
depositAddress: String,
): CryptoCurrencyStatus.Value {
val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = depositAddress,
),
)
return if (fiatRate != null) {
CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = fiatAmount,
fiatRate = fiatRate,
priceChange = BigDecimal.ZERO,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
)
} else {
CryptoCurrencyStatus.NoQuote(
amount = amount,
networkAddress = networkAddress,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
sources = CryptoCurrencyStatus.Sources(),
)
}
}
fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId } fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId } fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId }

View file

@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
* @property limit spending limit configuration for the card; `null` if not configured or not yet loaded. * @property limit spending limit configuration for the card; `null` if not configured or not yet loaded.
* @property frozenState whether the card is currently frozen (blocked for payments). * @property frozenState whether the card is currently frozen (blocked for payments).
* @property lastDigits The last four digits of the card number. * @property lastDigits The last four digits of the card number.
* @property state current lifecycle state of the card.
*/ */
@Serializable @Serializable
data class TangemPayCard( data class TangemPayCard(
@ -22,7 +23,7 @@ data class TangemPayCard(
@SerialName("limit") val limit: TangemPayCardLimitData?, @SerialName("limit") val limit: TangemPayCardLimitData?,
@SerialName("frozen_state") val frozenState: TangemPayCardFrozenState, @SerialName("frozen_state") val frozenState: TangemPayCardFrozenState,
@SerialName("last_digits") val lastDigits: String, @SerialName("last_digits") val lastDigits: String,
@SerialName("is_reissuing") val isReissuing: Boolean, @SerialName("state") val state: TangemPayCardState,
) )
val TangemPayCard.isFrozen val TangemPayCard.isFrozen

View file

@ -0,0 +1,38 @@
package com.tangem.domain.models.pay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
/**
* Lifecycle state of a Tangem Pay card.
*/
@Serializable
enum class TangemPayCardState {
/** Card is operational and ready to use. */
@SerialName("Active")
Active,
/** A reissue order is in progress; the card is being replaced. */
@SerialName("Reissuing")
Reissuing,
/** A close order is in progress; the card is being closed. */
@SerialName("Closing")
Closing,
;
override fun toString() = when (this) {
Active -> "Active"
Reissuing -> "Reissuing"
Closing -> "Closing"
}
companion object {
fun fromString(value: String) = when (value.lowercase(Locale.US)) {
"reissuing" -> Reissuing
"closing" -> Closing
else -> Active
}
}
}

View file

@ -51,7 +51,9 @@ class BalanceFetchingOperations(
async { async {
val result = when (source) { val result = when (source) {
FetchingSource.NETWORK -> fetchNetworks(userWalletId, currencies) FetchingSource.NETWORK -> fetchNetworks(userWalletId, currencies)
FetchingSource.QUOTE -> fetchQuotes(currencies) FetchingSource.QUOTE -> fetchQuotes(
currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
)
FetchingSource.STAKING -> fetchStaking(userWalletId, currencies) FetchingSource.STAKING -> fetchStaking(userWalletId, currencies)
} }
source to result source to result
@ -85,17 +87,14 @@ class BalanceFetchingOperations(
} }
/** /**
* Fetches quotes for the given currencies. * Fetches quotes for the given raw currency ids.
* *
* @param currencies the cryptocurrencies to fetch quotes for * @param rawCurrencyIds the raw currency ids to fetch quotes for
* @return Either with Unit on success or Throwable on failure * @return Either with Unit on success or Throwable on failure
*/ */
suspend fun fetchQuotes(currencies: Collection<CryptoCurrency>): Either<Throwable, Unit> { suspend fun fetchQuotes(rawCurrencyIds: Set<CryptoCurrency.RawID>): Either<Throwable, Unit> {
return multiQuoteStatusFetcher( return multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params( params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrencyIds, appCurrencyId = null),
currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
) )
} }

View file

@ -13,6 +13,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
@ -173,6 +174,7 @@ class WalletBalanceFetcher internal constructor(
// Fetch TangemPay separately — may run long-polling, so it must not block balance error checking // Fetch TangemPay separately — may run long-polling, so it must not block balance error checking
if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) { if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) {
balanceFetchingOperations.fetchQuotes(rawCurrencyIds = setOf(TangemPayCurrencyFactory.TOKEN_ID))
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
} }
} }

View file

@ -1,12 +0,0 @@
package com.tangem.domain.pay
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
@Deprecated("TangemPayCurrencyFactory")
interface TangemPayCryptoCurrencyFactory {
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.pay
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
/**
* Factory that builds the [CryptoCurrency.Token] used by Tangem Pay (USDC on Polygon) for a given user wallet.
*
* Replaces the deprecated `TangemPayCryptoCurrencyFactory`: callers no longer pass the chain id explicitly
* the underlying network is resolved from the wallet.
*/
interface TangemPayCurrencyFactory {
/**
* Builds the Tangem Pay token bound to the network of the wallet identified by [userWalletId].
*
* @throws IllegalStateException if no wallet with [userWalletId] is currently loaded.
*/
fun create(userWalletId: UserWalletId): CryptoCurrency.Token
/** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */
companion object {
/** CoinGecko-style raw id used to query quotes for the Tangem Pay token. */
val TOKEN_ID = CryptoCurrency.RawID("usd-coin")
const val TOKEN_NAME = "USDC"
const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
const val TOKEN_DECIMALS = 6
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.visa.error.VisaApiError
interface TangemPayCloseCardRepository {
suspend fun closeCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayOrderInfo>
suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit>
suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
}

View file

@ -15,8 +15,5 @@ interface TangemPayReissueCardRepository {
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit> suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
suspend fun getReissueOrderInfo( suspend fun getReissueOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayOrderInfo?>
} }

View file

@ -0,0 +1,33 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.launch
class CloseTangemPayCardUseCase(
private val closeCardRepository: TangemPayCloseCardRepository,
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val appCoroutineScope: AppCoroutineScope,
) {
suspend operator fun invoke(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, Unit> = either {
val order = closeCardRepository.closeCard(userWalletId, cardId).bind()
if (order.orderStatus == OrderStatus.CANCELED) {
raise(VisaApiError.Unspecified)
}
closeCardRepository.setCloseOrderId(cardId, order.orderId)
paymentAccountStatusFetcher.invoke(userWalletId)
appCoroutineScope.launch {
startTangemPayOrderPollingUseCase(order, userWalletId)
}
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.domain.pay.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class CloseTangemPayCardUseCaseTest {
private val closeCardRepository: TangemPayCloseCardRepository = mockk(relaxUnitFun = true)
private val startPollingUseCase: StartTangemPayOrderPollingUseCase = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
@Test
fun `GIVEN closeCard fails WHEN invoke THEN returns Left and skips store, fetch and polling`() = runTest {
val useCase = createUseCase()
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns VisaApiError.Unspecified.left()
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) }
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN closeCard returns CANCELED order WHEN invoke THEN returns Left and skips store, fetch and polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) }
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN closeCard returns PROCESSING order WHEN invoke THEN stores order id, fetches status and starts polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isRight()).isTrue()
coVerifyOrder {
closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID)
paymentAccountStatusFetcher.invoke(USER_WALLET_ID)
startPollingUseCase(order, USER_WALLET_ID)
}
}
@Test
fun `GIVEN closeCard returns COMPLETED order WHEN invoke THEN stores order id, fetches status and starts polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) }
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
coVerify(exactly = 1) { startPollingUseCase(order, USER_WALLET_ID) }
}
private fun createUseCase() = CloseTangemPayCardUseCase(
closeCardRepository = closeCardRepository,
startTangemPayOrderPollingUseCase = startPollingUseCase,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
appCoroutineScope = TestAppCoroutineScope(),
)
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
const val CARD_ID = "card-test-id"
const val ORDER_ID = "order-test-1"
}
}

View file

@ -6,26 +6,22 @@ sealed interface YieldBoostStatus {
data object NotStarted : YieldBoostStatus data object NotStarted : YieldBoostStatus
/** User entered boost, qualification period is still running. */ /**
data class Active( * User is enrolled in the boost (backend `active` or `completed`).
*
* The boost block on the active screen is driven entirely by [qualificationEndDate], which the backend
* computes as the end of the bonus-accrual period:
* - `null` nothing is shown;
* - in the future days left until the date;
* - reached / passed awaiting payout.
*/
data class Enrolled(
val tokenName: String, val tokenName: String,
val networkId: String, val networkId: String,
val moduleAddress: String, val moduleAddress: String,
val userAddress: String, val userAddress: String,
val contractAddress: String, val contractAddress: String,
val activationDate: Instant, val qualificationEndDate: Instant?,
val qualificationEndDate: Instant,
) : YieldBoostStatus
/** Boost has finished (backend `completed`). */
data class Completed(
val tokenName: String,
val networkId: String,
val moduleAddress: String,
val userAddress: String,
val contractAddress: String,
val activationDate: Instant,
val qualificationEndDate: Instant,
) : YieldBoostStatus ) : YieldBoostStatus
data class Disqualified(val reason: Reason) : YieldBoostStatus { data class Disqualified(val reason: Reason) : YieldBoostStatus {

View file

@ -6,7 +6,7 @@ import java.math.BigInteger
import java.math.RoundingMode import java.math.RoundingMode
private val HUNDRED_PERCENT = 100.toBigInteger() // base 100% private val HUNDRED_PERCENT = 100.toBigInteger() // base 100%
val INCREASE_GAS_LIMIT_FOR_SUPPLY = 120.toBigInteger() // 20% increase val INCREASE_GAS_LIMIT_FOR_SUPPLY = 140.toBigInteger() // 20% increase
fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = when (this) { fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = when (this) {
is Fee.Ethereum.Legacy -> copy( is Fee.Ethereum.Legacy -> copy(

View file

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

View file

@ -91,21 +91,10 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest {
} }
@Test @Test
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest { fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken() val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo() coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus() coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus()
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status is Completed WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns completedStatus()
val result = useCase(userWalletId, token) val result = useCase(userWalletId, token)
@ -162,26 +151,15 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest {
link = null, link = null,
) )
private fun activeStatus() = YieldBoostStatus.Active( private fun enrolledStatus() = YieldBoostStatus.Enrolled(
tokenName = "USD Coin", tokenName = "USD Coin",
networkId = networkRawId, networkId = networkRawId,
moduleAddress = "0xmodule", moduleAddress = "0xmodule",
userAddress = "0xuser", userAddress = "0xuser",
contractAddress = contractAddress, contractAddress = contractAddress,
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"), qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
) )
private fun completedStatus() = YieldBoostStatus.Completed(
tokenName = "USD Coin",
networkId = networkRawId,
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = contractAddress,
activationDate = Instant.parse("2026-04-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-05-01T00:00:00Z"),
)
private fun createToken( private fun createToken(
contractAddress: String = this.contractAddress, contractAddress: String = this.contractAddress,
networkRawId: String = this.networkRawId, networkRawId: String = this.networkRawId,

View file

@ -57,9 +57,9 @@ class ShouldShowYieldBoostMainBannerUseCaseTest {
} }
@Test @Test
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest { fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo() coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus() coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus()
val result = useCase(userWalletId) val result = useCase(userWalletId)
@ -92,13 +92,12 @@ class ShouldShowYieldBoostMainBannerUseCaseTest {
link = null, link = null,
) )
private fun activeStatus() = YieldBoostStatus.Active( private fun enrolledStatus() = YieldBoostStatus.Enrolled(
tokenName = "USD Coin", tokenName = "USD Coin",
networkId = networkRawId, networkId = networkRawId,
moduleAddress = "0xmodule", moduleAddress = "0xmodule",
userAddress = "0xuser", userAddress = "0xuser",
contractAddress = contractAddress, contractAddress = contractAddress,
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"), qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
) )
} }

View file

@ -25,11 +25,12 @@ interface SelectApprovalTypeComponent : ComposableBottomSheetComponent {
val cryptoCurrencyStatus: CryptoCurrencyStatus, val cryptoCurrencyStatus: CryptoCurrencyStatus,
val amountFooter: TextReference, val amountFooter: TextReference,
val initialApproveType: ApproveType = ApproveType.LIMITED, val initialApproveType: ApproveType = ApproveType.LIMITED,
val spenderAddress: String,
val callback: Callback, val callback: Callback,
) )
interface Callback { interface Callback {
fun onApproveTypeSelected(approveType: ApproveType) fun onApproveTypeSelected(spenderAddress: String, approveType: ApproveType)
fun onCancelClick() fun onCancelClick()
} }

View file

@ -43,7 +43,7 @@ internal class SelectApprovalTypeModel @Inject constructor(
} }
fun onConfirmClick() { fun onConfirmClick() {
params.callback.onApproveTypeSelected(uiState.value.approveType) params.callback.onApproveTypeSelected(params.spenderAddress, uiState.value.approveType)
} }
fun onCancelClick() { fun onCancelClick() {

View file

@ -1,14 +1,29 @@
package com.tangem.features.commonfeatures.api.addfunds package com.tangem.features.commonfeatures.api.addfunds
import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
interface AddFundsComponent : ComposableContentComponent { interface AddFundsComponent : ComposableBottomSheetComponent {
data class Params( data class Params(
val userWalletId: UserWalletId, val launchMode: LaunchMode,
val onDismiss: () -> Unit,
) )
sealed interface LaunchMode {
data class ChooseToken(val userWalletId: UserWalletId) : LaunchMode
data class TokenActionsOnly(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : LaunchMode
data class FilteredByRawId(
val rawCurrencyId: CryptoCurrency.RawID,
) : LaunchMode
}
interface Factory : ComponentFactory<Params, AddFundsComponent> interface Factory : ComponentFactory<Params, AddFundsComponent>
} }

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -94,7 +95,14 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
val wallet: UserWallet, val wallet: UserWallet,
val account: AccountStatus.CryptoPortfolio, val account: AccountStatus.CryptoPortfolio,
val addedCurrency: CryptoCurrencyStatus, val addedCurrency: CryptoCurrencyStatus,
val meta: FinishMeta = FinishMeta.None,
) )
sealed interface FinishMeta {
data object None : FinishMeta
data object OnQuickAction : FinishMeta
data class OnBottomAction(val action: BottomAction) : FinishMeta
}
} }
/** /**

View file

@ -29,6 +29,7 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal {
val title: TextReference, val title: TextReference,
val isShowMarketBlock: Boolean, val isShowMarketBlock: Boolean,
val isShowPaymentAccount: Boolean, val isShowPaymentAccount: Boolean,
val isAppBarShown: Boolean = true,
) { ) {
companion object { companion object {
val SwapFrom = Settings( val SwapFrom = Settings(
@ -45,6 +46,7 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal {
title = resourceReference(R.string.swapping_to_title), title = resourceReference(R.string.swapping_to_title),
isShowMarketBlock = true, isShowMarketBlock = true,
isShowPaymentAccount = false, isShowPaymentAccount = false,
isAppBarShown = false,
) )
} }
} }
@ -90,6 +92,11 @@ data class ChooseTokenResult(
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(), val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
) { ) {
val walletId get() = wallet.walletId val walletId get() = wallet.walletId
val wasJustAdded: Boolean
get() = analyticsPayload
.filterIsInstance<ChooseTokenAnalyticsPayload.IsMarketTokenSelected>()
.any { it.value }
} }
sealed interface ChooseTokenAnalyticsPayload { sealed interface ChooseTokenAnalyticsPayload {

View file

@ -0,0 +1,3 @@
package com.tangem.features.commonfeatures.api.tokenactions
enum class BottomAction { GoToToken, None }

View file

@ -1,95 +1,234 @@
package com.tangem.features.commonfeatures.impl.addfunds package com.tangem.features.commonfeatures.impl.addfunds
import androidx.compose.foundation.layout.Column import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemeRedesign import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent import com.tangem.features.commonfeatures.impl.addfunds.model.uiSpec
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import com.tangem.core.ui.R as CoreR
@Suppress("LongParameterList")
internal class DefaultAddFundsComponent @AssistedInject constructor( internal class DefaultAddFundsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AddFundsComponent.Params, @Assisted private val params: AddFundsComponent.Params,
chooseTokenComponentFactory: ChooseTokenComponent.Factory, chooseTokenComponentFactory: ChooseTokenComponent.Factory,
tokenActionsComponentFactory: TokenActionsComponent.Factory, tokenActionsComponentFactory: TokenActionsComponent.Factory,
userPortfolioComponentFactory: UserPortfolioComponent.Factory,
walletFeatureToggles: WalletFeatureToggles,
) : AppComponentContext by appComponentContext, AddFundsComponent { ) : AppComponentContext by appComponentContext, AddFundsComponent {
private val model: AddFundsModel = getOrCreateModel(params) private val model: AddFundsModel = getOrCreateModel(params)
private val chooseTokenComponent: ChooseTokenComponent = chooseTokenComponentFactory.create( private val isCompactTokenActions: Boolean = params.launchMode is AddFundsComponent.LaunchMode.TokenActionsOnly
context = child(key = "addFundsChooseToken"),
params = ChooseTokenComponent.Params(bridge = model.chooseTokenBridge),
)
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create( private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled
private val tokenActionsComponent: TokenActionsComponent by lazy {
tokenActionsComponentFactory.create(
context = child(key = "addFundsTokenActions"), context = child(key = "addFundsTokenActions"),
params = TokenActionsComponent.Params( params = TokenActionsComponent.Params(
data = model.tokenActionsData, data = model.tokenActionsData,
callbacks = model, callbacks = model,
bottomAction = TokenActionsComponent.BottomAction.GoToToken, bottomAction = model.currentBottomAction,
isRedesignForced = true, isRedesignForced = true,
isCompact = isCompactTokenActions,
), ),
) )
}
private val chooseTokenComponent: ChooseTokenComponent? by lazy {
(params.launchMode as? AddFundsComponent.LaunchMode.ChooseToken)?.let {
chooseTokenComponentFactory.create(
context = child(key = "addFundsChooseToken"),
params = ChooseTokenComponent.Params(
bridge = model.chooseTokenBridge,
),
)
}
}
private val userPortfolioComponent: UserPortfolioComponent by lazy {
userPortfolioComponentFactory.create(
context = child(key = "addFundsUserPortfolio"),
params = UserPortfolioComponent.Params(
uiState = model.userPortfolioStateController.uiState,
callbacks = object : UserPortfolioComponent.Callbacks {
override fun onContinueFromUserPortfolio() = Unit
},
),
)
}
override fun dismiss() = model.onDismiss()
@Composable @Composable
override fun Content(modifier: Modifier) { override fun BottomSheet() {
chooseTokenComponent.Content(modifier) val route by model.uiRoute.collectAsStateWithLifecycle()
val isTokenActionsShown by model.isTokenActionsShown.collectAsStateWithLifecycle() val canGoBack by model.canGoBack.collectAsStateWithLifecycle()
if (isTokenActionsShown) {
// force use redesign theme here according to the task requirements, will be reworked in the next release LaunchedEffect(route) {
TangemThemeRedesign { if (route != AddFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>( val mode = params.launchMode as? AddFundsComponent.LaunchMode.FilteredByRawId ?: return@LaunchedEffect
model.userPortfolioStateController.updateAndWaitNotNullState(
allAvailableData = model.buildAvailableToAddDataForChooser(),
rawCurrencyId = mode.rawCurrencyId,
)
}
WithOptionalRedesignTheme(isEnabled = isAddFundsStage1Enabled) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
onBack = if (canGoBack) model::onBack else ::dismiss,
config = TangemBottomSheetConfig( config = TangemBottomSheetConfig(
isShown = true, isShown = true,
onDismissRequest = model::onTokenActionsDismiss, onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty, content = TangemBottomSheetConfigContent.Empty,
), ),
type = when (params.launchMode) {
is AddFundsComponent.LaunchMode.TokenActionsOnly -> TangemBottomSheetType.Modal
is AddFundsComponent.LaunchMode.ChooseToken -> TangemBottomSheetType.Default
is AddFundsComponent.LaunchMode.FilteredByRawId ->
if (route is AddFundsModel.UiRoute.TokenActions) {
TangemBottomSheetType.Default
} else {
TangemBottomSheetType.Modal
}
},
containerColor = TangemTheme.colors2.surface.level2, containerColor = TangemTheme.colors2.surface.level2,
scrollableContent = true,
title = { title = {
TangemModalBottomSheetTitle( AddFundsBottomSheetTitle(
modifier = Modifier.fillMaxWidth(), route = route,
title = resourceReference(R.string.common_get_token), canGoBack = canGoBack,
endIconRes = R.drawable.ic_close_24, onBackClick = model::onBack,
onEndClick = model::onTokenActionsDismiss, onCloseClick = ::dismiss,
) )
}, },
content = { _ -> content = {
Column( val animatedContentModifier =
modifier = Modifier.padding( if (params.launchMode is AddFundsComponent.LaunchMode.ChooseToken) {
start = TangemTheme.dimens2.x4, Modifier.fillMaxSize()
top = TangemTheme.dimens2.x2, } else {
end = TangemTheme.dimens2.x4, Modifier
bottom = TangemTheme.dimens2.x4, }
), AnimatedContent(
targetState = route,
modifier = animatedContentModifier,
label = "AddFundsContentAnimation",
) { animatedRoute ->
AddFundsRouteContent(
route = animatedRoute,
shouldFillHeight = !isCompactTokenActions && animatedRoute.uiSpec().shouldFillHeight,
)
}
},
)
}
}
@Composable
private fun AddFundsRouteContent(route: AddFundsModel.UiRoute, shouldFillHeight: Boolean) {
val spec = route.uiSpec()
val horizontalPadding = if (spec.shouldApplyHorizontalPadding) {
Modifier.padding(horizontal = TangemTheme.dimens2.x4)
} else {
Modifier
}
val sizeModifier = if (shouldFillHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth()
RenderRoute(route, horizontalPadding.then(sizeModifier))
}
@Composable
private fun RenderRoute(route: AddFundsModel.UiRoute, modifier: Modifier = Modifier) {
when (route) {
AddFundsModel.UiRoute.Loading -> Unit
AddFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier)
AddFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider(
LocalTangemBottomSheetContentBottomInset provides TangemTheme.dimens2.x4,
) { ) {
tokenActionsComponent.Content(Modifier) userPortfolioComponent.Content(modifier)
} }
AddFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
}
}
@Composable
private fun AddFundsBottomSheetTitle(
route: AddFundsModel.UiRoute,
canGoBack: Boolean,
onBackClick: () -> Unit,
onCloseClick: () -> Unit,
) {
TangemTopBar(
title = route.uiSpec().title,
type = TangemTopBarType.BottomSheet,
startContent = if (canGoBack) {
{ CircleIconButton(iconRes = CoreR.drawable.ic_arrow_back_28, onClick = onBackClick) }
} else {
null
},
endContent = {
CircleIconButton(iconRes = R.drawable.ic_close_24, onClick = onCloseClick)
}, },
) )
} }
@Composable
private fun WithOptionalRedesignTheme(isEnabled: Boolean, content: @Composable () -> Unit) {
if (isEnabled) {
TangemThemeRedesign(content = content)
} else {
content()
} }
} }
@Composable
private fun CircleIconButton(iconRes: Int, onClick: () -> Unit) {
Icon(
imageVector = ImageVector.vectorResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = onClick)
.padding(TangemTheme.dimens2.x2),
)
}
@AssistedFactory @AssistedFactory
interface Factory : AddFundsComponent.Factory { interface Factory : AddFundsComponent.Factory {
override fun create(context: AppComponentContext, params: AddFundsComponent.Params): DefaultAddFundsComponent override fun create(context: AppComponentContext, params: AddFundsComponent.Params): DefaultAddFundsComponent

View file

@ -19,8 +19,6 @@ internal sealed class AddFundsAnalyticsEvent(
class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive") class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive")
class ButtonGoToToken : AddFundsAnalyticsEvent(event = "Button - Go to Token")
companion object { companion object {
private const val CATEGORY = "Add Funds" private const val CATEGORY = "Add Funds"
const val SOURCE_MAIN_SCREEN = "Main Screen" const val SOURCE_MAIN_SCREEN = "Main Screen"

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