Updated on 2026-08-14
This commit is contained in:
commit
788d7064b5
217 changed files with 6061 additions and 1892 deletions
|
|
@ -2,7 +2,7 @@
|
|||
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.
|
||||
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`
|
||||
|
|
@ -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
|
||||
- `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:
|
||||
- `java.io.IOException: Canceled` — normal request cancellation
|
||||
- `HttpException(code=304` — HTTP "Not Modified"
|
||||
- Bare stacktrace lines starting with `\tat`
|
||||
- `<-- 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
|
||||
|
||||
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)
|
||||
|
||||
## 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
|
||||
(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.)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
|
|||
step("Assert 'Buy' button is displayed") {
|
||||
onMainScreen { buyButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Swap' button is displayed") {
|
||||
onMainScreen { swapButton.assertIsDisplayed() }
|
||||
}
|
||||
|
|
@ -55,8 +58,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
|
|||
|
||||
fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) {
|
||||
if (isEnabled) {
|
||||
step("Assert 'Buy' button is enabled") {
|
||||
onMainScreen { buyButton.assertIsEnabled() }
|
||||
step("Assert 'Add funds' button is enabled") {
|
||||
onMainScreen { addFundsButton.assertIsEnabled() }
|
||||
}
|
||||
step("Assert 'Swap' button is enabled") {
|
||||
onMainScreen { swapButton.assertIsEnabled() }
|
||||
|
|
@ -65,8 +68,8 @@ fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean =
|
|||
onMainScreen { sellButton.assertIsEnabled() }
|
||||
}
|
||||
} else {
|
||||
step("Assert 'Buy' button is not enabled") {
|
||||
onMainScreen { buyButton.assertIsNotEnabled() }
|
||||
step("Assert 'Add funds' button is not enabled") {
|
||||
onMainScreen { addFundsButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Assert 'Swap' button is not enabled") {
|
||||
onMainScreen { swapButton.assertIsNotEnabled() }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
/**
|
||||
* "You receive" token chooser opened from the main-screen "Add funds" button.
|
||||
*/
|
||||
class ChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ChooseTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topAppBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val searchBar: KNode = child {
|
||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
}
|
||||
|
||||
fun tokenWithTitle(tokenTitle: String): KNode = child {
|
||||
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
hasAnyDescendant(withTestTag(TokenElementsTestTags.TOKEN_TITLE))
|
||||
hasAnyDescendant(withText(tokenTitle))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onChooseTokenScreen(function: ChooseTokenPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
/**
|
||||
* "Get token" bottom sheet shown after picking a token in the Add funds flow.
|
||||
* Contains quick actions (Buy / Receive / …) and the "Go to token" button.
|
||||
*/
|
||||
class GetTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<GetTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||
}
|
||||
|
||||
val closeButton: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
||||
// The "Get token" sheet action rows use combinedClickable; the row's testTag lands on a
|
||||
// separate zero-bounds semantics node that fails assertIsDisplayed. Matching the merged node
|
||||
// by its title text yields the displayed, clickable row (performClick injects a touch at its
|
||||
// center, which the row's clickable handles).
|
||||
val buyButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_buy))
|
||||
}
|
||||
|
||||
val receiveButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_receive))
|
||||
}
|
||||
|
||||
val goToTokenButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_go_to_token))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onGetTokenBottomSheet(function: GetTokenBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -55,6 +55,11 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addFundsButton: KNode = child {
|
||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||
hasText(getResourceString(R.string.common_add_funds))
|
||||
}
|
||||
|
||||
val sendButton: KNode = child {
|
||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class TangemPayAddFundsSheetPageObject(semanticsProvider: SemanticsNodeInteracti
|
|||
ComposeScreen<TangemPayAddFundsSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val swapOption: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.common_exchange))
|
||||
hasText(getResourceString(CoreResR.string.tangempay_topup_swap_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,15 +40,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert error notification title is displayed") {
|
||||
onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() }
|
||||
}
|
||||
|
|
@ -84,15 +87,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
|
|
@ -155,15 +161,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
|
|
@ -238,15 +247,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
|
|
@ -320,15 +332,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
|
|
@ -406,15 +421,18 @@ class BuyTokenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
onChooseTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Buy' in 'Get token' bottom sheet") {
|
||||
onGetTokenBottomSheet { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,17 +422,17 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Buy' button is displayed") {
|
||||
onMainScreen { buyButton.assertIsDisplayed() }
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.performClick() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.performClick() }
|
||||
}
|
||||
step("Assert 'Buy' screen title is displayed") {
|
||||
onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
||||
step("Assert 'Choose token' screen title is displayed") {
|
||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert token with title: '$tokenTitle' is displayed") {
|
||||
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() }
|
||||
onChooseTokenScreen { tokenWithTitle(tokenTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Press 'Back' button") {
|
||||
device.uiDevice.pressBack()
|
||||
|
|
@ -481,17 +481,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Buy' button is displayed") {
|
||||
onMainScreen { buyButton.assertIsDisplayed() }
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.performClick() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.performClick() }
|
||||
}
|
||||
step("Check 'Action is unavailable' dialog") {
|
||||
checkActionIsUnavailableDialog()
|
||||
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Ok' button") {
|
||||
onDialog { okButton.performClick() }
|
||||
step("Press 'Back' to return to main screen") {
|
||||
device.uiDevice.pressBack()
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Swap' button is displayed") {
|
||||
onMainScreen { swapButton.assertIsDisplayed() }
|
||||
|
|
@ -538,17 +539,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Buy' button is displayed") {
|
||||
onMainScreen { buyButton.assertIsDisplayed() }
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.performClick() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onMainScreen { addFundsButton.performClick() }
|
||||
}
|
||||
step("Check 'Action is unavailable' dialog") {
|
||||
checkActionIsUnavailableDialog()
|
||||
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Ok' button") {
|
||||
onDialog { okButton.performClick() }
|
||||
step("Press 'Back' to return to main screen") {
|
||||
device.uiDevice.pressBack()
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Swap' button is displayed") {
|
||||
onMainScreen { swapButton.assertIsDisplayed() }
|
||||
|
|
|
|||
|
|
@ -51,9 +51,12 @@ class HideTokenTest : BaseTestCase() {
|
|||
dialogContainer.assertIsDisplayed()
|
||||
okButton.clickWithAssertion()
|
||||
}
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is not displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(tokenTitle) }
|
||||
flakySafely {
|
||||
onMainScreen { assertTokenDoesNotExist(tokenTitle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.extensions.SwipeDirection
|
||||
import com.tangem.common.extensions.swipeVertical
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddAndManageBottomSheet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
|
|
@ -86,6 +88,15 @@ class MainScreenTest : BaseTestCase() {
|
|||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
||||
}
|
||||
step("Click 'Add & Manage' button") {
|
||||
onMainScreen { addAndManageButtonNode.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Organize tokens' option is not displayed (nothing to organize)") {
|
||||
onAddAndManageBottomSheet { organizeTokensButton.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert 'Add tokens' option is displayed") {
|
||||
onAddAndManageBottomSheet { addTokensButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
TangemLogger.i("onCreate")
|
||||
TangemLogger.i("onCreate: data=${intent?.data}, extras=${intent?.extras?.keySet()}")
|
||||
// We need to call it before onCreate to prevent unnecessary activity recreation
|
||||
installAppTheme()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ class AppsFlyerDeepLinkListener @Inject constructor(
|
|||
override fun onDeepLinking(p0: DeepLinkResult) {
|
||||
when (p0.status) {
|
||||
DeepLinkResult.Status.FOUND -> {
|
||||
referralParamsHandler.handle(deepLink = p0.deepLink)
|
||||
referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
}
|
||||
DeepLinkResult.Status.NOT_FOUND -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.i("No deep link found")
|
||||
}
|
||||
DeepLinkResult.Status.ERROR -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
|||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -23,14 +24,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun handle(deepLink: DeepLink) {
|
||||
handle(
|
||||
deepLinkValue = deepLink.deepLinkValue,
|
||||
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
|
||||
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
|
||||
)
|
||||
}
|
||||
private val deepLinkDeferred = CompletableDeferred<String?>()
|
||||
|
||||
fun handle(params: Map<String?, Any?>) {
|
||||
handle(
|
||||
|
|
@ -40,6 +34,31 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun handleDeeplink(deepLink: DeepLink) {
|
||||
handle(
|
||||
deepLinkValue = deepLink.deepLinkValue,
|
||||
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
|
||||
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
|
||||
)
|
||||
deepLinkDeferred.complete(deepLink.deepLinkValue)
|
||||
}
|
||||
|
||||
fun handleNoDeeplink() {
|
||||
deepLinkDeferred.complete(null)
|
||||
}
|
||||
|
||||
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
|
||||
val deeplinkFromCache = appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
return if (deeplinkFromCache == null) {
|
||||
val value = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
}
|
||||
deepLinkDeferred.await().takeIf { it == value }
|
||||
} else {
|
||||
deeplinkFromCache
|
||||
}
|
||||
}
|
||||
|
||||
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
|
||||
TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue")
|
||||
when (deepLinkValue) {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,20 @@ import android.app.Application
|
|||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
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.SensitiveUrlMasker
|
||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
||||
import com.tangem.utils.JsonStringValuesExtractor
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 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
|
||||
* interceptor
|
||||
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
|
||||
* @property environmentConfig source of [BlockchainSdkConfig] used to build the blockchain
|
||||
* URL masker
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TangemLoggingInitializer(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val tangemSdkLogger: TangemSdkLogger,
|
||||
private val environmentConfig: EnvironmentConfig,
|
||||
) {
|
||||
|
||||
fun initAppLogging() {
|
||||
|
|
@ -64,6 +72,13 @@ class TangemLoggingInitializer(
|
|||
}
|
||||
add(createNetworkLoggingInterceptor())
|
||||
add(ChuckerInterceptor(application))
|
||||
add(
|
||||
NetworkLogsSaveInterceptor(
|
||||
appLogsStore = appLogsStore,
|
||||
sensitiveUrlMasker = createBlockchainSensitiveUrlMasker(),
|
||||
shouldCheckResponseBodySize = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
TangemApiServiceSettings.addInterceptors(
|
||||
|
|
@ -77,4 +92,16 @@ class TangemLoggingInitializer(
|
|||
}.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,10 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
|||
import com.tangem.domain.card.BuildConfig
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
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.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
|
|
@ -34,8 +32,6 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
cardRepository: CardRepository,
|
||||
): TangemSdkManager {
|
||||
|
|
@ -49,8 +45,6 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
cardRepository = cardRepository,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.di.data
|
||||
|
||||
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.tap.common.log.TangemBlockchainSDKLogger
|
||||
import com.tangem.tap.common.log.TangemCardSDKLogger
|
||||
|
|
@ -17,10 +18,14 @@ internal object TangemLoggingModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLoggingInitializer(appLogsStore: AppLogsStore): TangemLoggingInitializer {
|
||||
fun provideLoggingInitializer(
|
||||
appLogsStore: AppLogsStore,
|
||||
environmentConfig: EnvironmentConfig,
|
||||
): TangemLoggingInitializer {
|
||||
return TangemLoggingInitializer(
|
||||
appLogsStore = appLogsStore,
|
||||
tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
|
||||
environmentConfig = environmentConfig,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
|||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
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.ScanResponse
|
||||
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.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -73,8 +73,6 @@ internal class DefaultTangemSdkManager(
|
|||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
private val cardRepository: CardRepository,
|
||||
) : TangemSdkManager {
|
||||
|
|
@ -145,12 +143,10 @@ internal class DefaultTangemSdkManager(
|
|||
runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(
|
||||
card = null,
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
||||
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
cardRepository = cardRepository,
|
||||
),
|
||||
|
|
@ -242,6 +238,7 @@ internal class DefaultTangemSdkManager(
|
|||
Analytics.send(event = analyticsEvent.withParams(params.toMap()))
|
||||
}
|
||||
.doOnFailure { tangemError ->
|
||||
TangemLogger.e("scanProduct failed: code=${tangemError.code}, message=${tangemError.customMessage}")
|
||||
(tangemError as? TangemSdkError)?.let { error ->
|
||||
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
|
||||
}
|
||||
|
|
@ -470,7 +467,6 @@ internal class DefaultTangemSdkManager(
|
|||
runnable = FinalizeTwinTask(
|
||||
twinPublicKey = secondCardPublicKey,
|
||||
issuerKeys = issuerKeyPair,
|
||||
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
||||
cardRepository = cardRepository,
|
||||
),
|
||||
cardId = cardId,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ import com.tangem.common.extensions.*
|
|||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvDecoder
|
||||
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.isNotSupportedInThatRelease
|
||||
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.backup.PrimaryCard
|
||||
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.files.ReadFilesTask
|
||||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class ScanProductTask(
|
||||
private val card: Card?,
|
||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||
private val visaCoroutineScope: CoroutineScope?,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||
private val shouldCheckIsAlreadyActivated: Boolean,
|
||||
private val isDynamicAddressesEnabled: Boolean,
|
||||
private val cardRepository: CardRepository,
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
|
@ -80,8 +74,6 @@ internal class ScanProductTask(
|
|||
session = session,
|
||||
cardDto = cardDto,
|
||||
scanWalletProcessor = ScanWalletProcessor(
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||
cardRepository = cardRepository,
|
||||
),
|
||||
callback = callback,
|
||||
|
|
@ -92,8 +84,6 @@ internal class ScanProductTask(
|
|||
val commandProcessor = when {
|
||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||
else -> ScanWalletProcessor(
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||
cardRepository = cardRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -102,8 +92,8 @@ internal class ScanProductTask(
|
|||
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
|
||||
when (scanTaskResult) {
|
||||
is CompletionResult.Success -> {
|
||||
// it needed because processorResult.data.card doesn't contains attestation result
|
||||
// and CardWallet.derivedKeys
|
||||
// It's needed because processorResult.data.card doesn't contain the attestation
|
||||
// result or the existing CardWallet.derivedKeys read from the card.
|
||||
val processorScanResponseWithNewCard = processorResult.data.copy(
|
||||
card = CardDTO(scanTaskResult.data),
|
||||
)
|
||||
|
|
@ -176,8 +166,6 @@ internal class ScanProductTask(
|
|||
}
|
||||
|
||||
private class ScanWalletProcessor(
|
||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||
private val isDynamicAddressesEnabled: Boolean,
|
||||
private val cardRepository: CardRepository,
|
||||
) : ProductCommandProcessor<ScanResponse> {
|
||||
|
||||
|
|
@ -281,48 +269,34 @@ private class ScanWalletProcessor(
|
|||
when (linkingResult) {
|
||||
is CompletionResult.Success -> {
|
||||
primaryCard = linkingResult.data
|
||||
deriveKeysIfNeeded(card, session, callback)
|
||||
completeScan(card, session, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
deriveKeysIfNeeded(card, session, callback)
|
||||
completeScan(card, session, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
val productType = getWalletProductType(card)
|
||||
scope.launch {
|
||||
val scanResponse = ScanResponse(
|
||||
card = card,
|
||||
productType = productType,
|
||||
walletData = session.environment.walletData,
|
||||
primaryCard = primaryCard,
|
||||
)
|
||||
val derivations = collectDerivations(card, scanResponse)
|
||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
val scanResponse = ScanResponse(
|
||||
card = card,
|
||||
productType = getWalletProductType(card),
|
||||
walletData = session.environment.walletData,
|
||||
primaryCard = primaryCard,
|
||||
)
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
}
|
||||
|
||||
private fun getWalletProductType(card: CardDTO): ProductType {
|
||||
|
|
@ -334,17 +308,6 @@ private class ScanWalletProcessor(
|
|||
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")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
|
|||
class FinalizeTwinTask(
|
||||
private val twinPublicKey: ByteArray,
|
||||
private val issuerKeys: KeyPair,
|
||||
private val isDynamicAddressesEnabled: Boolean,
|
||||
private val cardRepository: CardRepository,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
|
|
@ -31,11 +30,9 @@ class FinalizeTwinTask(
|
|||
is CompletionResult.Success ->
|
||||
ScanProductTask(
|
||||
card = readResult.data,
|
||||
blockchainToDeriveFinder = null,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
shouldCheckIsAlreadyActivated = false,
|
||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||
onboardingV2FeatureToggles = null,
|
||||
cardRepository = cardRepository,
|
||||
).run(session, callback)
|
||||
|
|
|
|||
|
|
@ -325,11 +325,7 @@ internal class DefaultUserWalletsListRepository(
|
|||
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
||||
.doOnSuccess { sensitiveInfo ->
|
||||
updateWallets { wallets ->
|
||||
// It is necessary to update derivations because when scanning we obtain the missing keys
|
||||
wallets?.updateWith(
|
||||
walletIdToSensitiveInformation = sensitiveInfo,
|
||||
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
|
||||
)
|
||||
wallets?.updateWith(walletIdToSensitiveInformation = sensitiveInfo)
|
||||
}
|
||||
trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
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.UserWalletId
|
||||
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.UserWalletSensitiveInformation
|
||||
|
||||
|
|
@ -74,10 +72,7 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
|
|||
return this.map { it.toUserWallet() }
|
||||
}
|
||||
|
||||
internal fun UserWallet.updateWith(
|
||||
sensitiveInformation: UserWalletSensitiveInformation,
|
||||
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
|
||||
): UserWallet {
|
||||
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
|
||||
return when (this) {
|
||||
is UserWallet.Cold -> {
|
||||
copy(
|
||||
|
|
@ -85,7 +80,6 @@ internal fun UserWallet.updateWith(
|
|||
card = scanResponse.card.copy(
|
||||
wallets = requireNotNull(sensitiveInformation.wallets),
|
||||
),
|
||||
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
|
||||
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
||||
),
|
||||
)
|
||||
|
|
@ -98,17 +92,14 @@ internal fun UserWallet.updateWith(
|
|||
|
||||
internal fun List<UserWallet>.updateWith(
|
||||
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
||||
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
|
||||
): List<UserWallet> {
|
||||
return if (walletIdToSensitiveInformation.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
this.map { wallet ->
|
||||
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
|
||||
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
|
||||
|
||||
if (sensitiveInformation != null) {
|
||||
wallet.updateWith(sensitiveInformation, derivedKeys)
|
||||
wallet.updateWith(sensitiveInformation)
|
||||
} else {
|
||||
wallet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -54,6 +53,7 @@ import com.tangem.hot.sdk.TangemHotSdk
|
|||
import com.tangem.hot.sdk.android.create
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerReferralParamsHandler
|
||||
import com.tangem.tap.features.hot.TangemHotSDKProxy
|
||||
import com.tangem.tap.features.root.RootDetectedWarningComponent
|
||||
import com.tangem.tap.features.scanfails.ScanFailsComponent
|
||||
|
|
@ -70,6 +70,8 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultRoutingComponent @AssistedInject constructor(
|
||||
|
|
@ -88,7 +90,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val appsFlyerReferralParamsHandler: AppsFlyerReferralParamsHandler,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val scanFailsComponentFactory: ScanFailsComponent.Factory,
|
||||
private val scanFailsRequesterProxy: ScanFailsRequesterProxy,
|
||||
|
|
@ -212,11 +214,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
|
||||
|
||||
if (isHotWalletOnboardingEnabled) {
|
||||
val tangemPayHotWalletOnboardingDeepLink = appsFlyerStore.getDeeplink(
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding,
|
||||
)
|
||||
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
|
||||
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
|
||||
if (tangemPayHotWalletOnboardingDeepLink != null) {
|
||||
val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.tangem.feature.stories.api.StoriesComponent
|
|||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
||||
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.ArchivedAccountListComponent
|
||||
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
|
||||
|
|
@ -116,7 +115,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val surveyComponentFactory: SurveyComponent.Factory,
|
||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||
private val addFundsComponentFactory: AddFundsComponent.Factory,
|
||||
) {
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -237,13 +235,6 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = buyCryptoComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.AddFunds -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = AddFundsComponent.Params(userWalletId = route.userWalletId),
|
||||
componentFactory = addFundsComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.SellCrypto -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
|
|||
|
|
@ -29,15 +29,19 @@ class AppsFlyerDeepLinkListenerTest {
|
|||
@ProvideTestModels
|
||||
fun onDeepLinking(model: OnDeepLinkingModel) = runTest {
|
||||
if (model.shouldHandle) {
|
||||
every { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } just Runs
|
||||
every { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) } just Runs
|
||||
} else {
|
||||
every { referralParamsHandler.handleNoDeeplink() } just Runs
|
||||
}
|
||||
|
||||
listener.onDeepLinking(p0 = model.deepLinkResult)
|
||||
|
||||
if (model.shouldHandle) {
|
||||
coVerify { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) }
|
||||
coVerify { referralParamsHandler.handleDeeplink(deepLink = model.deepLinkResult.deepLink) }
|
||||
verify(inverse = true) { referralParamsHandler.handleNoDeeplink() }
|
||||
} else {
|
||||
coVerify(inverse = true) { referralParamsHandler.handle(deepLink = any()) }
|
||||
coVerify(inverse = true) { referralParamsHandler.handleDeeplink(deepLink = any()) }
|
||||
verify { referralParamsHandler.handleNoDeeplink() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
|
|
@ -15,6 +17,7 @@ import io.mockk.mockk
|
|||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
|
|
@ -46,7 +49,7 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun handle(model: HandleDeepLinkModel) = runTest {
|
||||
handler.handle(deepLink = model.deepLink)
|
||||
handler.handleDeeplink(deepLink = model.deepLink)
|
||||
|
||||
if (model.shouldStore) {
|
||||
val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN)
|
||||
|
|
@ -165,6 +168,78 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
|
||||
data class HandleParamsModel(val params: Map<String?, Any?>, val shouldStore: Boolean)
|
||||
|
||||
@Nested
|
||||
inner class WaitForDeeplink {
|
||||
|
||||
private val localStore: AppsFlyerStore = mockk(relaxUnitFun = true)
|
||||
private val localHandler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = localStore,
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
setShouldShowMobileWalletPromoUseCase = mockk { coEvery { this@mockk.invoke(true) } returns Unit.right() },
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached deeplink WHEN waitForDeeplink THEN returns cached value`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
} returns "tpay_mobileonboard"
|
||||
|
||||
// WHEN
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo("tpay_mobileonboard")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache and matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns deeplink value`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
|
||||
val deepLink = mockk<DeepLink> {
|
||||
every { deepLinkValue } returns "tpay_mobileonboard"
|
||||
every { getStringValue(any()) } returns null
|
||||
}
|
||||
|
||||
// WHEN
|
||||
localHandler.handleDeeplink(deepLink)
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo("tpay_mobileonboard")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache and non-matching deeplink WHEN handleDeeplink then waitForDeeplink THEN returns null`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
|
||||
val deepLink = mockk<DeepLink> {
|
||||
every { deepLinkValue } returns "referral"
|
||||
every { getStringValue(any()) } returns null
|
||||
}
|
||||
|
||||
// WHEN
|
||||
localHandler.handleDeeplink(deepLink)
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache WHEN handleNoDeeplink then waitForDeeplink THEN returns null`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } returns null
|
||||
|
||||
// WHEN
|
||||
localHandler.handleNoDeeplink()
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
const val SUCCESS_REFCODE = "valid_refcode"
|
||||
const val SUCCESS_CAMPAIGN = "valid_campaign"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -59,11 +59,6 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data object Wallet : AppRoute(path = "/wallet")
|
||||
|
||||
@Serializable
|
||||
data class AddFunds(
|
||||
val userWalletId: UserWalletId,
|
||||
) : AppRoute(path = "/add_funds/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
data class CurrencyDetails(
|
||||
val userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TokenActionsHandler @AssistedInject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@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 messageSender: UiMessageSender,
|
||||
) {
|
||||
|
|
@ -49,6 +49,18 @@ class TokenActionsHandler @AssistedInject constructor(
|
|||
action = action,
|
||||
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
|
||||
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
|
||||
|
|
@ -164,7 +176,7 @@ class TokenActionsHandler @AssistedInject constructor(
|
|||
interface Factory {
|
||||
fun create(
|
||||
currentAppCurrency: Provider<AppCurrency>,
|
||||
onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
onHandleQuickAction: (HandledQuickAction, shouldDismiss: Boolean) -> Unit,
|
||||
): TokenActionsHandler
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,5 +122,9 @@
|
|||
{
|
||||
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15368_VISA_PAY_REDESIGN",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.coroutines.rx2)
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
/** Logging */
|
||||
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -97,6 +97,12 @@ interface TangemPayApi {
|
|||
@Body body: ReissueCardRequest,
|
||||
): 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")
|
||||
suspend fun getWithdrawData(
|
||||
@Header("Authorization") authHeader: String,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ data class YieldBoostStatusResponse(
|
|||
@Json(name = "userAddress") val userAddress: String?,
|
||||
@Json(name = "contractAddress") val contractAddress: String?,
|
||||
@Json(name = "promoEnrollmentStatus") val promoEnrollmentStatus: String,
|
||||
@Json(name = "activationDate") val activationDate: String?,
|
||||
@Json(name = "qualificationEndDate") val qualificationEndDate: String?,
|
||||
@Json(name = "disqualificationReason") val disqualificationReason: String?,
|
||||
)
|
||||
|
|
@ -3,8 +3,10 @@ package com.tangem.datasource.di
|
|||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
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.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
|
||||
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -32,4 +34,12 @@ internal object TangemPayStoresModule {
|
|||
prefs = prefs,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayCloseCardStore(prefs: AppPreferencesStore): TangemPayCloseCardStore {
|
||||
return DefaultTangemPayCloseCardStore(
|
||||
prefs = prefs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,11 +16,15 @@ import com.tangem.datasource.api.utils.ConnectTimeout
|
|||
import com.tangem.datasource.api.utils.ReadTimeout
|
||||
import com.tangem.datasource.api.utils.WriteTimeout
|
||||
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.SensitiveUrlMasker
|
||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||
import com.tangem.datasource.utils.addHeaders
|
||||
import com.tangem.utils.JsonStringValuesExtractor
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Invocation
|
||||
|
|
@ -41,6 +45,7 @@ import javax.inject.Singleton
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Singleton
|
||||
internal class RetrofitApiBuilder @Inject constructor(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
|
|
@ -49,10 +54,20 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val environmentConfig: EnvironmentConfig,
|
||||
) {
|
||||
|
||||
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
|
||||
*
|
||||
|
|
@ -179,7 +194,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
|
||||
private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder {
|
||||
return addInterceptor(
|
||||
interceptor = NetworkLogsSaveInterceptor(appLogsStore),
|
||||
interceptor = NetworkLogsSaveInterceptor(appLogsStore, sensitiveUrlMasker),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.P2PKeys
|
||||
import com.tangem.datasource.local.config.environment.models.SurveySparrowSwapRatingConfig
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@Serializable
|
||||
data class EnvironmentConfig(
|
||||
val moonPayApiKey: String = "",
|
||||
val moonPayApiSecretKey: String = "",
|
||||
|
|
@ -32,6 +35,7 @@ data class EnvironmentConfig(
|
|||
val gaslessTxApiKey: String? = null,
|
||||
val customerIoCdpApiKey: String? = null,
|
||||
val surveySparrowToken: String? = null,
|
||||
@Transient
|
||||
val surveySparrowSwapRating: SurveySparrowSwapRatingConfig? = null,
|
||||
val authServiceKey: String? = null,
|
||||
)
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.datasource.local.config.environment.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String)
|
||||
|
||||
@Serializable
|
||||
data class P2PKeys(val mainnet: String, val hoodi: String)
|
||||
|
||||
data class SurveySparrowSwapRatingConfig(
|
||||
|
|
|
|||
|
|
@ -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 = "******"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ sealed interface PaymentAccountStatusValueDM {
|
|||
@Json(name = "deposit_address") val depositAddress: String?,
|
||||
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
|
||||
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
|
||||
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
|
||||
@Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
|
||||
@Json(name = "cards") val cards: List<TangemPayCard>,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
|
@ -58,6 +59,7 @@ sealed interface PaymentAccountStatusValueDM {
|
|||
@NameLabel("deactivated_account")
|
||||
data class DeactivatedAccount(
|
||||
@Json(name = "deactivated_account") val marker: Boolean = true,
|
||||
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
|
||||
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
|
||||
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
|
@ -86,6 +88,6 @@ sealed interface PaymentAccountStatusValueDM {
|
|||
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
|
||||
@Json(name = "frozen_state") val frozenState: String,
|
||||
@Json(name = "last_digits") val lastDigits: String,
|
||||
@Json(name = "is_reissuing") val isReissuing: Boolean,
|
||||
@Json(name = "state") val state: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.logs.SensitiveUrlMasker
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
|
@ -22,11 +24,15 @@ private const val JSON_INDENT_SPACES = 4
|
|||
* Interceptor for save network requests and responses logs
|
||||
*
|
||||
* @property appLogsStore app logs store
|
||||
* @property sensitiveUrlMasker masker for sensitive data in URLs
|
||||
* @property shouldCheckResponseBodySize whether to skip logging large response bodies
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NetworkLogsSaveInterceptor(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val sensitiveUrlMasker: SensitiveUrlMasker? = null,
|
||||
private val shouldCheckResponseBodySize: Boolean = false,
|
||||
) : Interceptor {
|
||||
|
||||
@Throws(IOException::class)
|
||||
|
|
@ -65,7 +71,7 @@ class NetworkLogsSaveInterceptor(
|
|||
val connection = chain.connection()
|
||||
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) {
|
||||
|
|
@ -73,7 +79,7 @@ class NetworkLogsSaveInterceptor(
|
|||
val connectionProtocol = if (connection != null) " ${connection.protocol()}" else ""
|
||||
|
||||
saveLogMessage(
|
||||
"--> ${request.method} ${request.url}$connectionProtocol\n",
|
||||
"--> ${request.method} ${request.url.maskSensitiveInfo()}$connectionProtocol\n",
|
||||
createRequestEndMessage(request),
|
||||
)
|
||||
}
|
||||
|
|
@ -110,7 +116,7 @@ class NetworkLogsSaveInterceptor(
|
|||
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
|
||||
saveLogMessage(
|
||||
"<-- ${response.code}",
|
||||
" ${response.request.url} (${tookMs}ms)\n",
|
||||
" ${response.request.url.maskSensitiveInfo()} (${tookMs}ms)\n",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -123,39 +129,45 @@ class NetworkLogsSaveInterceptor(
|
|||
"<-- END HTTP"
|
||||
} else if (bodyHasUnknownEncoding(response.headers)) {
|
||||
"<-- END HTTP (encoded body omitted)"
|
||||
} else if (shouldCheckResponseBodySize && contentLength > WRITE_LOG_THRESHOLD_BYTES_SIZE) {
|
||||
"Response size too large: $contentLength bytes \n<-- END HTTP"
|
||||
} else {
|
||||
val source = responseBody.source()
|
||||
source.request(Long.MAX_VALUE)
|
||||
var buffer = source.buffer
|
||||
|
||||
var gzippedLength: Long? = null
|
||||
if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) {
|
||||
gzippedLength = buffer.size
|
||||
GzipSource(buffer.clone()).use { gzippedResponseBody ->
|
||||
buffer = Buffer()
|
||||
buffer.writeAll(gzippedResponseBody)
|
||||
}
|
||||
}
|
||||
|
||||
val contentType = responseBody.contentType()
|
||||
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
|
||||
|
||||
if (!buffer.isProbablyUtf8()) {
|
||||
"<-- END HTTP (binary ${buffer.size}-byte body omitted)"
|
||||
if (shouldCheckResponseBodySize && buffer.size > WRITE_LOG_THRESHOLD_BYTES_SIZE) {
|
||||
"Response size too large: ${buffer.size} bytes \n<-- END HTTP"
|
||||
} else {
|
||||
val json = if (contentLength != 0L) {
|
||||
buffer.clone().readString(charset).beautifyJson()
|
||||
} else {
|
||||
""
|
||||
var gzippedLength: Long? = null
|
||||
if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) {
|
||||
gzippedLength = buffer.size
|
||||
GzipSource(buffer.clone()).use { gzippedResponseBody ->
|
||||
buffer = Buffer()
|
||||
buffer.writeAll(gzippedResponseBody)
|
||||
}
|
||||
}
|
||||
|
||||
val end = if (gzippedLength != null) {
|
||||
"<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)"
|
||||
} else {
|
||||
"<-- END HTTP (${buffer.size}-byte body)"
|
||||
}
|
||||
val contentType = responseBody.contentType()
|
||||
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
|
||||
|
||||
"$json\n$end"
|
||||
if (!buffer.isProbablyUtf8()) {
|
||||
"<-- END HTTP (binary ${buffer.size}-byte body omitted)"
|
||||
} else {
|
||||
val json = if (contentLength != 0L) {
|
||||
buffer.clone().readString(charset).beautifyJson()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val end = if (gzippedLength != null) {
|
||||
"<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)"
|
||||
} else {
|
||||
"<-- END HTTP (${buffer.size}-byte body)"
|
||||
}
|
||||
|
||||
"$json\n$end"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,12 +178,16 @@ class NetworkLogsSaveInterceptor(
|
|||
saveLogMessage(
|
||||
"<-- ${response.code}",
|
||||
spaceBeforeResponseMessage,
|
||||
response.message,
|
||||
" ${response.request.url} (${tookMs}ms)\n",
|
||||
" ${response.request.url.maskSensitiveInfo()} (${tookMs}ms)\n",
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpUrl.maskSensitiveInfo(): String {
|
||||
val url = toString()
|
||||
return sensitiveUrlMasker?.mask(url) ?: url
|
||||
}
|
||||
|
||||
private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
|
||||
val contentEncoding = headers["Content-Encoding"] ?: return false
|
||||
return !contentEncoding.equals("identity", ignoreCase = true) &&
|
||||
|
|
@ -231,6 +247,9 @@ class NetworkLogsSaveInterceptor(
|
|||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val WRITE_LOG_THRESHOLD_BYTES_SIZE = 2_048_000L
|
||||
|
||||
/**
|
||||
* List of URLs (host + path) for which logging is restricted
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -91,6 +91,7 @@
|
|||
<string name="add_custom_token_title">Token anlegen</string>
|
||||
<string name="add_tokens_title">Token verwalten</string>
|
||||
<string name="addfunds_buy_row_description">Kreditkarte oder Bankkonto</string>
|
||||
<string name="addfunds_fund_token">Token erhalten</string>
|
||||
<string name="addfunds_receive_row_description">Teile deine Adresse oder dein QR-Code</string>
|
||||
<string name="addfunds_swap_row_description">Zwische deinen Portfolios</string>
|
||||
<string name="addfunds_you_receive_title">Empfangen</string>
|
||||
|
|
@ -662,6 +663,11 @@
|
|||
<string name="feedback_subject_support_tangem">Feedback zu Tangem</string>
|
||||
<string name="feedback_subject_tx_failed">Eine Transaktion kann nicht gesendet werden</string>
|
||||
<string name="feedback_token_description_error">Fehler in der Coinbeschreibung</string>
|
||||
<string name="force_update_banner_message">Aktualisiere die Anwendung auf die neueste Version, um die ordnungsgemäße Funktionalität zu gewährleisten</string>
|
||||
<string name="force_update_banner_title">Aktualisierung erforderlich</string>
|
||||
<string name="force_update_button">Update</string>
|
||||
<string name="force_update_warning_message">Bitte aktualisiere die Anwendung auf die neueste Version, um eine einwandfreie Funktion zu gewährleisten.</string>
|
||||
<string name="force_update_warning_title">Aktualisierung erforderlich</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Nicht genügend Mittel</string>
|
||||
<string name="gasless_transaction_fee">Transaktionsgebühr</string>
|
||||
<string name="generic_error">Es ist ein Fehler aufgetreten</string>
|
||||
|
|
@ -787,6 +793,8 @@
|
|||
<string name="koinos_mana_level_description">Das Koinos-Netzwerk benötigt Mana als Netzwerkgebühr. Du hast %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Mana-Level</string>
|
||||
<string name="main_add_and_manage_tokens">Hinzufügen und Verwalten</string>
|
||||
<string name="main_add_funds_promo_description">Krypto einzahlen oder mit Karte kaufen, um loszulegen</string>
|
||||
<string name="main_add_funds_promo_title">Hol dir deine erste Kryptowährung</string>
|
||||
<string name="main_empty_tokens_list_message">Um mit der Verfolgung deiner Krypto-Assets und -Transaktionen zu beginnen, füge einen Token hinzu</string>
|
||||
<string name="main_manage_tokens">Token verwalten</string>
|
||||
<string name="main_qr_scan_hint">QR-Code scannen, um Geld zu senden oder eine Verbindung zu einer App herzustellen</string>
|
||||
|
|
@ -1073,7 +1081,7 @@
|
|||
<string name="onboarding_create_wallet_options_button_options">Andere Optionen</string>
|
||||
<string name="onboarding_create_wallet_options_message">Deine Schlüssel(private-keys) werden sicher im Inneren der Karte oder Ring generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen.</string>
|
||||
<string name="onboarding_create_wallet_options_title">Schlüssel anonym generieren</string>
|
||||
<string name="onboarding_create_wallet_term_of_conditions_text">Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:%s</string>
|
||||
<string name="onboarding_create_wallet_term_of_conditions_text">Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:\n%s</string>
|
||||
<string name="onboarding_done_body">Deine Karte oder Ring ist aktiviert und einsatzbereit</string>
|
||||
<string name="onboarding_done_header">Erfolgreich!</string>
|
||||
<string name="onboarding_done_wallet">Deine Wallet ist eingerichtet und einsatzbereit!</string>
|
||||
|
|
@ -1210,9 +1218,7 @@
|
|||
<string name="organize_tokens_title">Token organisieren</string>
|
||||
<string name="organize_tokens_ungroup">Gruppe löschen</string>
|
||||
<string name="provider_name_support">%s Unterstützung</string>
|
||||
<string name="push_notification_settings_banner_button_grant_permission">Genehmigung erteilen</string>
|
||||
<string name="push_notification_settings_banner_description">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast.</string>
|
||||
<string name="push_notification_settings_banner_description_grant_permission">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst nach Ihrer Zustimmung.</string>
|
||||
<string name="push_notification_settings_banner_title">Benachrichtigungen zulassen</string>
|
||||
<string name="push_notification_settings_offers_updates_subtitle">Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten.</string>
|
||||
<string name="push_notification_settings_offers_updates_title">Angebote & Updates</string>
|
||||
|
|
@ -1684,11 +1690,13 @@
|
|||
<string name="tangem_pay_freeze_card_failed">Karte konnte nicht eingefroren werden. Versuchen Sie es später erneut.</string>
|
||||
<string name="tangem_pay_freeze_card_freeze">Einfrieren</string>
|
||||
<string name="tangem_pay_freeze_card_success">Ihre Karte ist eingefroren.</string>
|
||||
<string name="tangem_pay_freeze_card_unfreeze">Aufheben</string>
|
||||
<string name="tangem_pay_get_help">Hilfe erhalten</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Grund: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Andere</string>
|
||||
<string name="tangem_pay_pin_code_title">PIN-Code</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Nicht nutzbar auf gerooteten Geräten</string>
|
||||
<string name="tangem_pay_status_completed">Abgeschlossen</string>
|
||||
<string name="tangem_pay_status_declined">Abgelehnt</string>
|
||||
|
|
@ -1697,15 +1705,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Bedingungen, Gebühren & Limits</string>
|
||||
<string name="tangem_pay_terms_limits">Bedingungen und Einschränkungen</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">Die Bank hat diese Transaktionsanfrage abgelehnt.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Eine Gebühr wird gemäß den Servicetarifen erhoben</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">Die Transaktion wurde vom Händler teilweise oder vollständig storniert</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Karte entsperren?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Ihre Karte ist entsperrt.</string>
|
||||
<string name="tangem_pay_withdrawal">Abhebung</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Dies wurde aufgrund regulatorischer Anforderungen durchgeführt. Auszahlungen sind jedoch weiterhin verfügbar.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Ihre Karte wurde deaktiviert</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Bei Fragen zu Ihrem Konto, Ihren Daten oder Ihrem Transaktionsverlauf wenden Sie sich bitte an den Support</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Ihr Konto wurde geschlossen</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Auf gerooteten Geräten nicht nutzbar.</string>
|
||||
<string name="tangempay_available_balance">Verfügbares Guthaben</string>
|
||||
<string name="tangempay_cancel_kyc">KYC vom Hauptbildschirm ausblenden</string>
|
||||
|
|
@ -1739,7 +1747,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Karte zu Google Pay hinzufügen</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Karte zu Apple Pay hinzufügen</string>
|
||||
<string name="tangempay_card_details_pin_code">Pin Code</string>
|
||||
<string name="tangempay_card_details_receive_description">Teile Deine Adresse mit oder zeig den QR-Code.</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Es wurden technische Probleme festgestellt. Bitte versuche es später erneut oder kontaktiere den Support.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Empfangen ist jetzt nicht verfügbar</string>
|
||||
<string name="tangempay_card_details_reissue_card">Karte neu ausstellen</string>
|
||||
|
|
@ -1748,7 +1755,6 @@
|
|||
<string name="tangempay_card_details_rename_card_placeholder">Kartenname</string>
|
||||
<string name="tangempay_card_details_reveal_text">Aufdecken</string>
|
||||
<string name="tangempay_card_details_show_details">Details anzeigen</string>
|
||||
<string name="tangempay_card_details_swap_description">Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte.</string>
|
||||
<string name="tangempay_card_details_title">Kartendetails</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Bitte versuche es später noch einmal.</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Karte entsperren</string>
|
||||
|
|
@ -1766,6 +1772,7 @@
|
|||
<string name="tangempay_card_page_daily_limit_change">Ändern</string>
|
||||
<string name="tangempay_card_page_daily_limit_current_limit">Aktuelles Limit</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_description">Ihr Tageslimit konnte nicht geladen werden. Bitte versuchen Sie es erneut.</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_subtitle">Neu laden und es erneut versuchen</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_title">Tageslimit nicht verfügbar</string>
|
||||
<string name="tangempay_card_page_daily_limit_success_description">Sie können es jederzeit wieder ändern</string>
|
||||
<string name="tangempay_card_page_daily_limit_success_title">Tageslimit ist festgelegt</string>
|
||||
|
|
@ -1827,7 +1834,7 @@
|
|||
<string name="tangempay_onboarding_security_title">Unerreichte Privatsphäre</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_description">Verknüpfen Sie eine Zahlungskarte</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">Wir richten eine Wallet ein.</string>
|
||||
<string name="tangempay_onboarding_title">Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten</string>
|
||||
<string name="tangempay_onboarding_title">Holen Sie sich Ihre Tangem Pay Karte</string>
|
||||
<string name="tangempay_pay_support">Bezahlen mit</string>
|
||||
<string name="tangempay_payment_account">Zahlungskonto</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay sitzung abgelaufen</string>
|
||||
|
|
@ -1851,7 +1858,6 @@
|
|||
<string name="tangempay_sync_needed">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
|
||||
<string name="tangempay_sync_needed_body">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
|
||||
<string name="tangempay_sync_needed_button">Zugang wiederherstellen</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Zugang wiederherstellen</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay sitzung abgelaufen</string>
|
||||
<string name="tangempay_tangem_visa_card">Nutzen Sie USDC für alltägliche Zahlungen</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht erreichbar.</string>
|
||||
|
|
@ -1861,7 +1867,6 @@
|
|||
<string name="tangempay_topup_swap_body">Tauschen Sie beliebige Assets in USDC Polygon um</string>
|
||||
<string name="tangempay_topup_swap_title">Aus Ihrer Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC im Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen</string>
|
||||
<string name="tangempay_withdrawal_note_description">Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar</string>
|
||||
<string name="tangempay_withdrawal_note_title">Bitte beachten Sie</string>
|
||||
<string name="tangempay_your_pin_code">Ihr PIN-Code</string>
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@
|
|||
<string name="common_select_action">Seleccione una acción</string>
|
||||
<string name="common_sell">Vender</string>
|
||||
<string name="common_send">Enviar</string>
|
||||
<string name="common_send_colon">Enviar:</string>
|
||||
<string name="common_send_tx_error">Error al enviar la transacción</string>
|
||||
<string name="common_server_unavailable">El servidor no está disponible, por favor inténtelo de nuevo más tarde</string>
|
||||
<string name="common_share">Compartir</string>
|
||||
|
|
@ -1609,15 +1610,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Términos, tarifas y límites</string>
|
||||
<string name="tangem_pay_terms_limits">Términos y límites</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">El banco rechazó esta solicitud de transacción.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Esta tarifa cubre el costo de procesar tu transferencia.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Se cobra una comisión de acuerdo con las tarifas de servicio</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">La transacción fue revertida parcial o totalmente por el comerciante</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Sigue usando tu dinero. Puedes congelarlo en cualquier momento.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">¿Descongelar tu tarjeta?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Tu tarjeta está descongelada.</string>
|
||||
<string name="tangem_pay_withdrawal">Retirada</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Esto se hizo debido a requisitos regulatorios. Sin embargo, los retiros siguen estando disponibles.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Su tarjeta ha sido desactivada</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Para consultas sobre su cuenta, datos o historial de transacciones, contacte con el soporte</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Su cuenta ha sido cerrada</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">No se puede usar en un dispositivo rooteado</string>
|
||||
<string name="tangempay_available_balance">Saldo</string>
|
||||
<string name="tangempay_cancel_kyc">Ocultar verificación de la pantalla</string>
|
||||
|
|
@ -1650,7 +1651,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Añadir tarjeta a Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Añade tu tarjeta a Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">Código PIN</string>
|
||||
<string name="tangempay_card_details_receive_description">Comparte tu dirección o muestra el código QR</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Se detectaron problemas técnicos. Inténtelo de nuevo más tarde o póngase en contacto con el servicio de asistencia.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Recepción no disponible ahora</string>
|
||||
<string name="tangempay_card_details_reissue_card">Reemitir tarjeta</string>
|
||||
|
|
@ -1658,7 +1658,6 @@
|
|||
<string name="tangempay_card_details_rename_card_invalid_title">Caracteres no válidos</string>
|
||||
<string name="tangempay_card_details_reveal_text">Mostrar</string>
|
||||
<string name="tangempay_card_details_show_details">Mostrar detalles</string>
|
||||
<string name="tangempay_card_details_swap_description">Intercambia cualquier activo de tu portafolio por una tarjeta</string>
|
||||
<string name="tangempay_card_details_title">Detalles de la tarjeta</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Por favor, inténtalo de nuevo más tarde</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Descongelar tarjeta</string>
|
||||
|
|
@ -1719,7 +1718,7 @@
|
|||
<string name="tangempay_onboarding_purchases_title">Paga exactamente lo que ves</string>
|
||||
<string name="tangempay_onboarding_security_description">Se creará una cuenta de pago separada sin divulgar tus direcciones y activos</string>
|
||||
<string name="tangempay_onboarding_security_title">Privacidad inigualable</string>
|
||||
<string name="tangempay_onboarding_title">Obtén tu tarjeta Tangem Pay gratuita en minutos</string>
|
||||
<string name="tangempay_onboarding_title">Obtén tu tarjeta Tangem Pay en minutos</string>
|
||||
<string name="tangempay_payment_account">Cuenta de pago</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay sesión expirada</string>
|
||||
<string name="tangempay_pin_validation_error_message">PIN no válido: evitar secuencias o repeticiones</string>
|
||||
|
|
@ -1741,7 +1740,6 @@
|
|||
<string name="tangempay_sync_needed">Usa la tarjeta o el anillo para renovar la sesión</string>
|
||||
<string name="tangempay_sync_needed_body">Usa la tarjeta o el anillo para renovar la sesión</string>
|
||||
<string name="tangempay_sync_needed_button">Restablecer acceso</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Restablecer acceso</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay sesión expirada</string>
|
||||
<string name="tangempay_tangem_visa_card">Usa USDC para pagos cotidianos</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</string>
|
||||
|
|
@ -1751,7 +1749,6 @@
|
|||
<string name="tangempay_topup_swap_body">Intercambia cualquier activo por USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">Desde tu Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC en Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Haga clic en el botón de abajo para restaurar el acceso</string>
|
||||
<string name="tangempay_withdrawal_note_description">Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain Polygon ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras</string>
|
||||
<string name="tangempay_withdrawal_note_title">Tenga en cuenta</string>
|
||||
<string name="tangempay_your_pin_code">Tu código PIN</string>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
<string name="action_buttons_swap_not_enough_tokens_alert_title">Ajouter des jetons</string>
|
||||
<string name="action_buttons_you_want_to_receive">Sélectionnez le jeton que vous souhaitez recevoir</string>
|
||||
<string name="action_buttons_you_want_to_swap">Sélectionnez le jeton que vous souhaitez échanger</string>
|
||||
<string name="add_and_manage_sheet_manage_title">Ajouter des jetons</string>
|
||||
<string name="add_custom_token_choose_network">Choisissez le réseau</string>
|
||||
<string name="add_custom_token_title">Ajouter un jeton personnalisé</string>
|
||||
<string name="add_tokens_title">Gérer les jetons</string>
|
||||
|
|
@ -367,6 +368,7 @@
|
|||
<string name="common_select_action">Sélectionnez une action</string>
|
||||
<string name="common_sell">Vendre</string>
|
||||
<string name="common_send">Envoyer</string>
|
||||
<string name="common_send_colon">Vous envoyez :</string>
|
||||
<string name="common_send_tx_error">Échec d\'envoi de la transaction</string>
|
||||
<string name="common_server_unavailable">Le serveur n\'est pas disponible, veuillez réessayer plus tard</string>
|
||||
<string name="common_share">Partager</string>
|
||||
|
|
@ -549,6 +551,7 @@
|
|||
<string name="express_provider">Fournisseur</string>
|
||||
<string name="express_provider_best_rate">Meilleur taux</string>
|
||||
<string name="express_provider_fca_warning_list">Liste d’avertissement de la FCA</string>
|
||||
<string name="express_provider_for_swap">Prestataire pour l\'échange</string>
|
||||
<string name="express_provider_great_rate">Meilleur choix</string>
|
||||
<string name="express_provider_in_fca_warning_list">Fournisseur figurant sur la liste d\'avertissement de la FCA</string>
|
||||
<string name="express_provider_max_amount">Disponible jusqu\'à %s</string>
|
||||
|
|
@ -711,6 +714,7 @@
|
|||
<string name="koinos_mana_exceeds_koin_balance_title">Limite de Mana</string>
|
||||
<string name="koinos_mana_level_description">Le réseau Koinos nécessite du Mana pour les frais de réseau. Vous avez %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Quantité de Mana</string>
|
||||
<string name="main_add_and_manage_tokens">Ajouter & gérer</string>
|
||||
<string name="main_empty_tokens_list_message">Pour commencer à suivre vos actifs et transactions crypto, ajoutez des jetons</string>
|
||||
<string name="main_manage_tokens">Gérer les jetons</string>
|
||||
<string name="main_scan_card_warning_view_subtitle">Pour accéder à tous les réseaux, vous devez scanner la carte</string>
|
||||
|
|
@ -1047,16 +1051,29 @@
|
|||
<string name="onramp_error_transaction_already_processed">Cette transaction a déjà été traitée. Aucune autre action n\'est requise.</string>
|
||||
<string name="onramp_fetching_best_rates">Recherche des meilleurs tarifs...</string>
|
||||
<string name="onramp_instant_status">Instantané</string>
|
||||
<string name="onramp_kyc_verification_bullet_free">La vérification est gratuite et prend généralement entre 1 et 2 minutes</string>
|
||||
<string name="onramp_kyc_verification_bullet_privacy">Tangem n\'a pas accès à vos données personnelles, vous les partagez directement au prestataire agréé</string>
|
||||
<string name="onramp_kyc_verification_bullet_unlocks">La vérification vous donne un accès complet aux futures transactions avec ce prestataire</string>
|
||||
<string name="onramp_kyc_verification_choose_another">Sélectionner une autre méthode</string>
|
||||
<string name="onramp_kyc_verification_subtitle">Conformément aux exigences réglementaires locales, %@ exige une vérification d\'identité.</string>
|
||||
<string name="onramp_kyc_verification_title">Vérification d\'identité requise par le prestataire de paiement</string>
|
||||
<string name="onramp_kyc_verification_verify_button">Passer la vérification</string>
|
||||
<string name="onramp_kyc_verification_whats_important">Ce qui est important</string>
|
||||
<string name="onramp_legal">En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur</string>
|
||||
<string name="onramp_legal_text">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
|
||||
<string name="onramp_max_amount_restriction">Le montant de l\'achat ne doit pas dépasser %s</string>
|
||||
<string name="onramp_min_amount_restriction">Le montant à acheter doit être au moins %s</string>
|
||||
<string name="onramp_native_payment_cumulative_limit" formatted="false">Si le montant cumulé des transactions dépasse %1s, une vérification d\'identité via %2s pourrait être requise</string>
|
||||
<string name="onramp_native_payment_cumulative_limit_equivalent" formatted="false">Si le montant cumulé des transactions dépasse l\'équivalent de %1s, une vérification d\'identité via %2s pourrait être requise</string>
|
||||
<string name="onramp_native_payment_legal_notice" formatted="false">En appuyant sur Acheter, vous acceptez %1s %2s et %3s.</string>
|
||||
<string name="onramp_no_available_providers">Aucun fournisseur disponible pour cette devise</string>
|
||||
<string name="onramp_offer_type_fastet">Le plus rapide</string>
|
||||
<string name="onramp_pay_with">Payer avec</string>
|
||||
<string name="onramp_payment_method_subtitle">Mode de paiement</string>
|
||||
<string name="onramp_provider_max_amount">Disponible jusqu\'à %s</string>
|
||||
<string name="onramp_provider_min_amount">Disponible à partir de %s</string>
|
||||
<string name="onramp_provider_requirements_body">Les cartes émises aux États-Unis et au Royaume-Uni ne peuvent pas être traitées par ce moyen. Le prestataire pourrait exiger une vérification d\'identité supplémentaire</string>
|
||||
<string name="onramp_provider_requirements_title">Exigences du prestataire</string>
|
||||
<plurals name="onramp_providers_count">
|
||||
<item quantity="one">%d fournisseur</item>
|
||||
<item quantity="other">%d fournisseurs</item>
|
||||
|
|
@ -1456,6 +1473,7 @@
|
|||
<string name="sui_not_enough_coin_for_fee_description">Une transaction entrante d\'au moins de %1$s est requise pour continuer</string>
|
||||
<string name="sui_not_enough_coin_for_fee_title">Fonds insuffisants</string>
|
||||
<string name="swap_approve_description">En approuvant, vous autorisez le contrat intelligent à utiliser vos jetons dans de futures transactions.</string>
|
||||
<string name="swap_detailed_mode">Mode détaillé</string>
|
||||
<string name="swap_fixed_rate">Taux fixe</string>
|
||||
<string name="swap_give_permission_fee_footer">Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange.</string>
|
||||
<string name="swap_in_progress">Échange en cours</string>
|
||||
|
|
@ -1463,6 +1481,7 @@
|
|||
<string name="swap_promo_title">Nouveau fournisseur d\'échange disponible !</string>
|
||||
<string name="swap_search_tooltip_description">Recherchez n’importe quel token, même s’il ne figure pas encore dans votre liste.</string>
|
||||
<string name="swap_search_tooltip_title">Utilisez la recherche pour trouver ce dont vous avez besoin.</string>
|
||||
<string name="swap_simple_mode">Mode simplifié</string>
|
||||
<string name="swap_story_fifth_subtitle">Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes</string>
|
||||
<string name="swap_story_fifth_title">Assistance 24 heures sur 24</string>
|
||||
<string name="swap_story_first_subtitle">Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement</string>
|
||||
|
|
@ -1534,15 +1553,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Conditions, frais et limites</string>
|
||||
<string name="tangem_pay_terms_limits">Conditions et limites</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">La banque a rejeté cette demande de transaction.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Ces frais couvrent le coût du traitement de votre virement.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Des frais sont prélevés conformément aux tarifs de service</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">La transaction a été partiellement ou totalement annulée par le commerçant</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Continuez à utiliser votre argent. Vous pouvez le geler à tout moment.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Dégeler votre carte ?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Échec du dégel de la carte. Réessayez plus tard.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Votre carte est dégelée.</string>
|
||||
<string name="tangem_pay_withdrawal">Retrait</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Cela a été fait conformément aux exigences réglementaires. Toutefois, les retraits restent disponibles.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Votre carte a été désactivée</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Pour toute question concernant votre compte, vos données ou votre historique de transactions, veuillez contacter le support</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Votre compte a été fermé</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Impossible à utiliser sur un appareil rooté</string>
|
||||
<string name="tangempay_available_balance">Solde</string>
|
||||
<string name="tangempay_cancel_kyc">Masquer la vérification de l\'écran</string>
|
||||
|
|
@ -1574,7 +1593,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Ajouter une carte à Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Ajouter la carte à Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">code PIN</string>
|
||||
<string name="tangempay_card_details_receive_description">Partagez votre adresse ou montrez le QR code</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Problèmes techniques détectés. Veuillez réessayer plus tard ou contacter le service d\'assistance.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Réception indisponible pour le moment</string>
|
||||
<string name="tangempay_card_details_reissue_card">Réémettre la carte</string>
|
||||
|
|
@ -1582,7 +1600,6 @@
|
|||
<string name="tangempay_card_details_rename_card_invalid_title">Caractères non valides</string>
|
||||
<string name="tangempay_card_details_reveal_text">Révéler</string>
|
||||
<string name="tangempay_card_details_show_details">Afficher les détails</string>
|
||||
<string name="tangempay_card_details_swap_description">Échangez n\'importe quel actif de votre portefeuille contre une carte</string>
|
||||
<string name="tangempay_card_details_title">Détails de la carte</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Veuillez réessayer plus tard</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Dégeler la carte</string>
|
||||
|
|
@ -1643,7 +1660,7 @@
|
|||
<string name="tangempay_onboarding_purchases_title">Payez exactement ce que vous voyez</string>
|
||||
<string name="tangempay_onboarding_security_description">Un compte de paiement séparé sera créé sans divulguer vos adresses et actifs</string>
|
||||
<string name="tangempay_onboarding_security_title">Confidentialité inégalée</string>
|
||||
<string name="tangempay_onboarding_title">Obtenez votre carte Tangem Pay gratuite en quelques minutes</string>
|
||||
<string name="tangempay_onboarding_title">Obtenez votre carte Tangem Pay en minutes</string>
|
||||
<string name="tangempay_payment_account">Compte de paiement</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay session expirée</string>
|
||||
<string name="tangempay_pin_validation_error_message">Code PIN invalide : évitez les séquences ou les répétitions</string>
|
||||
|
|
@ -1665,7 +1682,6 @@
|
|||
<string name="tangempay_sync_needed">Utilisez carte ou bague pour renouveler la session</string>
|
||||
<string name="tangempay_sync_needed_body">Utilisez carte ou bague pour renouveler la session</string>
|
||||
<string name="tangempay_sync_needed_button">Restaurer l\'accès</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Restaurer l\'accès</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay session expirée</string>
|
||||
<string name="tangempay_tangem_visa_card">Utilisez USDC pour les paiements quotidiens</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay est temporairement indisponible</string>
|
||||
|
|
@ -1675,7 +1691,6 @@
|
|||
<string name="tangempay_topup_swap_body">Échangez n\'importe quel actif contre USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">Depuis votre Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC sur Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Cliquez sur le bouton ci-dessous pour restaurer l\'accès</string>
|
||||
<string name="tangempay_withdrawal_note_description">Les fonds des achats remboursés ne seront pas retournés à votre solde sur Polygon ni disponibles pour un retrait, mais resteront sur votre solde de carte pour vos achats</string>
|
||||
<string name="tangempay_withdrawal_note_title">Veuillez noter</string>
|
||||
<string name="tangempay_your_pin_code">Votre code PIN</string>
|
||||
|
|
|
|||
|
|
@ -94,15 +94,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Termini, commissioni e limiti</string>
|
||||
<string name="tangem_pay_terms_limits">Termini e limiti</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">La banca ha rifiutato questa richiesta di transazione.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Questa commissione copre il costo della gestione del tuo trasferimento.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Viene addebitata una commissione in base alle tariffe del servizio</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">La transazione è stata parzialmente o totalmente stornata dal commerciante</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Continua a usare i tuoi soldi. Puoi congelarli in qualsiasi momento.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Sbloccare la tua carta?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Impossibile sbloccare la carta. Riprova più tardi.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">La tua carta è sbloccata.</string>
|
||||
<string name="tangem_pay_withdrawal">Prelievo</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Questo è stato fatto a causa dei requisiti normativi. Tuttavia, i prelievi sono ancora disponibili.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">La tua carta è stata disattivata</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Per domande su account, dati o cronologia delle transazioni, contatta il supporto</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Il tuo account è stato chiuso</string>
|
||||
<string name="tangempay_available_balance">Saldo</string>
|
||||
<string name="tangempay_cancel_kyc">Nascondi verifica dalla schermata</string>
|
||||
<string name="tangempay_card_details_add_funds">Aggiungi fondi</string>
|
||||
|
|
@ -132,14 +132,12 @@
|
|||
<string name="tangempay_card_details_open_wallet_step_5">Tutto pronto! La tua carta è pronta per l\'uso.</string>
|
||||
<string name="tangempay_card_details_open_wallet_title">Aggiungi carta a Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Aggiungi carta ad Apple Pay</string>
|
||||
<string name="tangempay_card_details_receive_description">Condividi il tuo indirizzo o mostra il QR code</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Ricezione non disponibile al momento</string>
|
||||
<string name="tangempay_card_details_reissue_card">Riemettere la carta</string>
|
||||
<string name="tangempay_card_details_rename_card_invalid_description">Sono consentite solo lettere e numeri</string>
|
||||
<string name="tangempay_card_details_rename_card_invalid_title">Caratteri non validi</string>
|
||||
<string name="tangempay_card_details_reveal_text">Rivela</string>
|
||||
<string name="tangempay_card_details_show_details">Mostra dettagli</string>
|
||||
<string name="tangempay_card_details_swap_description">Scambia qualsiasi asset nel tuo portafoglio con una carta</string>
|
||||
<string name="tangempay_card_details_title">Dettagli carta</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Per favore riprova più tardi</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Sblocca carta</string>
|
||||
|
|
@ -194,7 +192,7 @@
|
|||
<string name="tangempay_onboarding_purchases_title">Paga esattamente quello che vedi</string>
|
||||
<string name="tangempay_onboarding_security_description">Verrà creato un conto di pagamento separato senza divulgare i tuoi indirizzi e asset</string>
|
||||
<string name="tangempay_onboarding_security_title">Privacy senza rivali</string>
|
||||
<string name="tangempay_onboarding_title">Ottieni la tua carta Tangem Pay gratuita in pochi minuti</string>
|
||||
<string name="tangempay_onboarding_title">Ottieni la tua carta Tangem Pay in pochi minuti</string>
|
||||
<string name="tangempay_payment_account">Conto di pagamento</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay sessione scaduta</string>
|
||||
<string name="tangempay_pin_validation_error_message">PIN non valido: evitare sequenze o ripetizioni</string>
|
||||
|
|
@ -223,7 +221,6 @@
|
|||
<string name="tangempay_topup_swap_body">Converti qualsiasi asset in USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">Dal tuo Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC sulla Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Fare clic sul pulsante in basso per ripristinare l\'accesso</string>
|
||||
<string name="tangempay_withdrawal_note_description">I fondi degli acquisti rimborsati non verranno restituiti al tuo saldo on-chain Polygon né saranno disponibili per il prelievo, ma rimarranno sul saldo della tua carta per gli acquisti</string>
|
||||
<string name="tangempay_withdrawal_note_title">Attenzione</string>
|
||||
<string name="tangempay_your_pin_code">Il tuo codice PIN</string>
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@
|
|||
<string name="common_sending">送金中</string>
|
||||
<string name="common_sent">送金済み</string>
|
||||
<string name="common_server_unavailable">サーバーが利用できません。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="common_session_expired">セッションの有効期限が切れました</string>
|
||||
<string name="common_share">共有</string>
|
||||
<string name="common_share_link">リンクを共有</string>
|
||||
<string name="common_show_less">詳細を非表示</string>
|
||||
|
|
@ -776,6 +777,8 @@
|
|||
<string name="koinos_mana_level_description">Koinosネットワークでは、ネットワーク手数料としてManaが必要です。あなたは%1$s / %2$sManaを持っています。</string>
|
||||
<string name="koinos_mana_level_title">Manaレベル</string>
|
||||
<string name="main_add_and_manage_tokens">追加・管理</string>
|
||||
<string name="main_add_funds_promo_description">暗号資産を入金またはカードで購入</string>
|
||||
<string name="main_add_funds_promo_title">入金して、運用や取引を始めましょう。</string>
|
||||
<string name="main_empty_tokens_list_message">暗号資産および取引の追跡を開始するには、トークンを追加してください</string>
|
||||
<string name="main_manage_tokens">トークンの管理</string>
|
||||
<string name="main_qr_scan_hint">QRコードをスキャンして送金するか、アプリに接続します。</string>
|
||||
|
|
@ -1187,9 +1190,7 @@
|
|||
<string name="organize_tokens_title">トークンを整理する</string>
|
||||
<string name="organize_tokens_ungroup">グループ解除</string>
|
||||
<string name="provider_name_support">%sサポート</string>
|
||||
<string name="push_notification_settings_banner_button_grant_permission">許可する</string>
|
||||
<string name="push_notification_settings_banner_description">プッシュ通知は有効になっていますが、端末の設定で通知を許可するまで動作しません。</string>
|
||||
<string name="push_notification_settings_banner_description_grant_permission">プッシュ通知は有効になっていますが、許可するまで機能しません</string>
|
||||
<string name="push_notification_settings_banner_description">プッシュ通知は有効ですが、許可するまで動作しません</string>
|
||||
<string name="push_notification_settings_banner_title">通知を許可する</string>
|
||||
<string name="push_notification_settings_offers_updates_subtitle">製品ニュース、限定オファー、アクティビティのリマインダー。</string>
|
||||
<string name="push_notification_settings_offers_updates_title">オファー・最新情報</string>
|
||||
|
|
@ -1628,7 +1629,7 @@
|
|||
<string name="swapping_rate_experience_title">プロバイダーの利用体験を評価してください</string>
|
||||
<string name="swapping_rate_feedback_placeholder">フィードバックを入力してください</string>
|
||||
<string name="swapping_rate_feedback_submit">フィードバックを送信</string>
|
||||
<string name="swapping_rate_feedback_title">ご利用体験に影響した点は\n何ですか?</string>
|
||||
<string name="swapping_rate_feedback_title">ご利用中に気になった点を\n教えてください</string>
|
||||
<string name="swapping_swap_action">スワップ</string>
|
||||
<string name="swapping_swap_action_in_progress">スワップ中…</string>
|
||||
<string name="swapping_to_account_title">受け取り先</string>
|
||||
|
|
@ -1671,15 +1672,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">利用規約・手数料・利用制限</string>
|
||||
<string name="tangem_pay_terms_limits">利用規約と手数料</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">銀行がこの取引リクエストを拒否しました。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">この手数料は、送金処理にかかるコストをカバーするためのものです。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">手数料はサービス料金に基づいて請求されます</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">この取引は加盟店により一部または全額取り消されました</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">資金は引き続き使用できます。いつでも一時停止できます。</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">カードの一時停止を解除しますか?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">カードの凍結が解除されました</string>
|
||||
<string name="tangem_pay_withdrawal">出金</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">規制上の要件により無効化されましたが、出金は引き続き可能です。</string>
|
||||
<string name="tangempay_account_deactivated_message_title">カードが無効化されました</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">アカウント、データ、または取引履歴に関するご質問は、サポートまでご連絡ください</string>
|
||||
<string name="tangempay_account_deactivated_message_title">あなたのアカウントは閉鎖されました</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Root化された端末では使用できません</string>
|
||||
<string name="tangempay_available_balance">利用可能残高</string>
|
||||
<string name="tangempay_cancel_kyc">メイン画面からKYCを非表示にする</string>
|
||||
|
|
@ -1713,7 +1714,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Google Payにカードを追加する</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Apple Payにカードを追加する</string>
|
||||
<string name="tangempay_card_details_pin_code">PINコード</string>
|
||||
<string name="tangempay_card_details_receive_description">アドレスを共有するか、QRコードを表示してください。</string>
|
||||
<string name="tangempay_card_details_receive_error_description">技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。</string>
|
||||
<string name="tangempay_card_details_receive_error_title">現在、受け取りは利用できません</string>
|
||||
<string name="tangempay_card_details_reissue_card">カードを交換する</string>
|
||||
|
|
@ -1722,7 +1722,6 @@
|
|||
<string name="tangempay_card_details_rename_card_placeholder">カード名</string>
|
||||
<string name="tangempay_card_details_reveal_text">表示</string>
|
||||
<string name="tangempay_card_details_show_details">詳細を表示</string>
|
||||
<string name="tangempay_card_details_swap_description">ポートフォリオ内のあらゆる資産をカードと交換</string>
|
||||
<string name="tangempay_card_details_title">カードの詳細</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">しばらくしてからもう一度お試しください</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">カードの一時停止を解除</string>
|
||||
|
|
@ -1800,7 +1799,7 @@
|
|||
<string name="tangempay_onboarding_security_title">他に類を見ないプライバシー</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_description">そして支払いカードを連携します</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">ウォレットを設定します</string>
|
||||
<string name="tangempay_onboarding_title">無料のTangem Payカードを数分でゲットしましょう</string>
|
||||
<string name="tangempay_onboarding_title">Tangem Pay カードをすぐに手に入れよう</string>
|
||||
<string name="tangempay_pay_support">Payサポート</string>
|
||||
<string name="tangempay_payment_account">支払いアカウント</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay セッションの有効期限が切れました</string>
|
||||
|
|
@ -1824,17 +1823,15 @@
|
|||
<string name="tangempay_sync_needed">カードまたはリングでセッションを更新してください</string>
|
||||
<string name="tangempay_sync_needed_body">カードまたはリングでセッションを更新してください</string>
|
||||
<string name="tangempay_sync_needed_button">セッションを更新</string>
|
||||
<string name="tangempay_sync_needed_restore_access">セッションを更新</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay セッションの有効期限が切れました</string>
|
||||
<string name="tangempay_tangem_visa_card">日常の支払いにUSDCを利用</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Payは現在一時的に利用できません。</string>
|
||||
<string name="tangempay_title">Tangem Pay</string>
|
||||
<string name="tangempay_topup_receive_body">USDC Polygon をアカウントのアドレスに送信</string>
|
||||
<string name="tangempay_topup_receive_title">別のウォレットまたは取引所から</string>
|
||||
<string name="tangempay_topup_swap_body">任意の資産を USDC Polygon にスワップ</string>
|
||||
<string name="tangempay_topup_swap_title">Tangem ウォレットから</string>
|
||||
<string name="tangempay_topup_swap_body">ウォレットの暗号資産を使って、決済アカウントにチャージできます</string>
|
||||
<string name="tangempay_topup_swap_title">Tangemウォレットからスワップ</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">Polygonネットワーク上のUSDC</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">下のボタンをクリックしてアクセスを復元してください</string>
|
||||
<string name="tangempay_withdrawal_note_description">返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。</string>
|
||||
<string name="tangempay_withdrawal_note_title">ご注意ください</string>
|
||||
<string name="tangempay_your_pin_code">PINコード</string>
|
||||
|
|
@ -2346,6 +2343,10 @@
|
|||
<string name="yield_apy_boost_banner_title">利息モード限定オファー</string>
|
||||
<string name="yield_apy_boost_banner_title_apy_multiplied">APY 3倍</string>
|
||||
<string name="yield_apy_boost_block_activate">APYブーストを有効にする</string>
|
||||
<string name="yield_apy_boost_promo_activate_bonus">ボーナスを有効にする</string>
|
||||
<string name="yield_apy_boost_promo_bonus_paid_out_subtitle">詳細は取引履歴をご確認ください</string>
|
||||
<string name="yield_apy_boost_promo_bonus_paid_out_title">利息モードのボーナスが支払われました</string>
|
||||
<string name="yield_apy_boost_promo_days_left_to_unlock">ボーナス獲得まであと%1$s日</string>
|
||||
<string name="yield_apy_boost_promo_eligibility_text">30日間APYブーストの対象です。利用規約が適用されます。詳細はこちら。</string>
|
||||
<string name="yield_apy_boost_story_first_subtitle">初めて利息モードを有効にすると、最初の30日間は最大3倍の利回りを獲得できます。</string>
|
||||
<string name="yield_apy_boost_story_first_title">初月APRボーナス</string>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
<string name="add_custom_token_title">Adicionar token personalizado</string>
|
||||
<string name="add_tokens_title">Gerenciar tokens</string>
|
||||
<string name="addfunds_buy_row_description">Cartão de crédito ou conta bancária</string>
|
||||
<string name="addfunds_fund_token">Adicionar token</string>
|
||||
<string name="addfunds_receive_row_description">Compartilhe seu endereço ou código QR.</string>
|
||||
<string name="addfunds_swap_row_description">Entre seus portfólios</string>
|
||||
<string name="addfunds_you_receive_title">Você recebe</string>
|
||||
|
|
@ -227,7 +228,7 @@
|
|||
<string name="common_action_failed">%s fracassado</string>
|
||||
<string name="common_activate">Ativar</string>
|
||||
<string name="common_add">Adicionar</string>
|
||||
<string name="common_add_funds">Adicionar fundos</string>
|
||||
<string name="common_add_funds">Depositar</string>
|
||||
<string name="common_add_to_portfolio">Adicionar ao portfólio</string>
|
||||
<string name="common_add_token">Adicionar token</string>
|
||||
<string name="common_add_tokens">Adicionar tokens</string>
|
||||
|
|
@ -662,6 +663,11 @@
|
|||
<string name="feedback_subject_support_tangem">Feedback Tangem</string>
|
||||
<string name="feedback_subject_tx_failed">Não foi possível enviar uma transação.</string>
|
||||
<string name="feedback_token_description_error">Erro na descrição da moeda</string>
|
||||
<string name="force_update_banner_message">Atualize o aplicativo para a versão mais recente para garantir o funcionamento correto.</string>
|
||||
<string name="force_update_banner_title">Atualização necessária</string>
|
||||
<string name="force_update_button">Atualizar</string>
|
||||
<string name="force_update_warning_message">Por favor, atualize o aplicativo para a versão mais recente para garantir o funcionamento correto.</string>
|
||||
<string name="force_update_warning_title">Atualização necessária</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Fundos insuficientes</string>
|
||||
<string name="gasless_transaction_fee">Taxa de transação</string>
|
||||
<string name="generic_error">Ocorreu um erro.</string>
|
||||
|
|
@ -787,6 +793,8 @@
|
|||
<string name="koinos_mana_level_description">A rede Koinos exige Mana para o pagamento das taxas de rede. Você tem %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Nível de mana</string>
|
||||
<string name="main_add_and_manage_tokens">Adicionar e gerenciar</string>
|
||||
<string name="main_add_funds_promo_description">Compre ou receba criptomoedas para começar a usar sua carteira.</string>
|
||||
<string name="main_add_funds_promo_title">Adquira suas primeiras criptomoedas.</string>
|
||||
<string name="main_empty_tokens_list_message">Para começar a rastrear seus criptoativos e transações, adicione tokens.</string>
|
||||
<string name="main_manage_tokens">Gerenciar tokens</string>
|
||||
<string name="main_qr_scan_hint">Leia o código QR para enviar fundos ou conectar-se a um aplicativo</string>
|
||||
|
|
@ -1073,7 +1081,7 @@
|
|||
<string name="onboarding_create_wallet_options_button_options">Outras opções</string>
|
||||
<string name="onboarding_create_wallet_options_message">Suas chaves serão geradas com segurança dentro do chip. Não há frase mnemônica, o que significa que ninguém pode exportá-las ou roubá-las.</string>
|
||||
<string name="onboarding_create_wallet_options_title">Gere chaves de forma privada</string>
|
||||
<string name="onboarding_create_wallet_term_of_conditions_text">Ao continuar, você concorda com os termos. %s</string>
|
||||
<string name="onboarding_create_wallet_term_of_conditions_text">Ao continuar, você concorda com os termos.\n%s</string>
|
||||
<string name="onboarding_done_body">Seu cartão está ativado e pronto para uso.</string>
|
||||
<string name="onboarding_done_header">Sucesso!</string>
|
||||
<string name="onboarding_done_wallet">Sua carteira está configurada e pronta para uso!</string>
|
||||
|
|
@ -1210,9 +1218,7 @@
|
|||
<string name="organize_tokens_title">Organizar tokens</string>
|
||||
<string name="organize_tokens_ungroup">Desagrupar</string>
|
||||
<string name="provider_name_support">%s suporte</string>
|
||||
<string name="push_notification_settings_banner_button_grant_permission">Conceder permissão</string>
|
||||
<string name="push_notification_settings_banner_description">As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo.</string>
|
||||
<string name="push_notification_settings_banner_description_grant_permission">As notificações push estão ativadas, mas não funcionarão até que você conceda permissão.</string>
|
||||
<string name="push_notification_settings_banner_title">Permitir notificações</string>
|
||||
<string name="push_notification_settings_offers_updates_subtitle">Novidades sobre produtos, ofertas exclusivas e lembretes de atividades.</string>
|
||||
<string name="push_notification_settings_offers_updates_title">Ofertas e atualizações</string>
|
||||
|
|
@ -1684,11 +1690,13 @@
|
|||
<string name="tangem_pay_freeze_card_failed">Não foi possível bloquear o cartão. Tente novamente mais tarde.</string>
|
||||
<string name="tangem_pay_freeze_card_freeze">Congelar</string>
|
||||
<string name="tangem_pay_freeze_card_success">Seu cartão está bloqueado.</string>
|
||||
<string name="tangem_pay_freeze_card_unfreeze">Descongelar</string>
|
||||
<string name="tangem_pay_get_help">Obtenha ajuda</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Razão: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Outro</string>
|
||||
<string name="tangem_pay_pin_code_title">Código PIN</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Não é possível usar em dispositivos com root.</string>
|
||||
<string name="tangem_pay_status_completed">Concluído</string>
|
||||
<string name="tangem_pay_status_declined">Recusado</string>
|
||||
|
|
@ -1697,15 +1705,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Termos, taxas e limites</string>
|
||||
<string name="tangem_pay_terms_limits">Termos e Limites</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">O banco rejeitou esta solicitação de transação.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Essa taxa destina-se a cobrir os custos de processamento da sua transferência.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Uma taxa é cobrada de acordo com as tarifas de serviço</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">A transação foi parcial ou totalmente revertida pelo comerciante.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Continue usando seu dinheiro. Você pode congelar a qualquer momento.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Descongelar seu cartão?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Não foi possível desbloquear o cartão. Tente novamente mais tarde.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Seu cartão foi desbloqueado.</string>
|
||||
<string name="tangem_pay_withdrawal">Retirada</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Isso foi feito devido a requisitos regulatórios. No entanto, saques ainda estão disponíveis.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Seu cartão foi desativado</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Para dúvidas sobre sua conta, dados ou histórico de transações, entre em contato com o suporte</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Sua conta foi encerrada</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Não é possível usar em dispositivos com root.</string>
|
||||
<string name="tangempay_available_balance">Saldo disponível</string>
|
||||
<string name="tangempay_cancel_kyc">Ocultar KYC da tela principal</string>
|
||||
|
|
@ -1739,7 +1747,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Adicionar cartão ao Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Adicionar cartão ao Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">Código PIN</string>
|
||||
<string name="tangempay_card_details_receive_description">Compartilhe seu endereço ou mostre o código QR.</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Problemas técnicos detectados. Tente novamente mais tarde ou entre em contato com o suporte.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Receber indisponível agora</string>
|
||||
<string name="tangempay_card_details_reissue_card">Substituir cartão</string>
|
||||
|
|
@ -1748,7 +1755,6 @@
|
|||
<string name="tangempay_card_details_rename_card_placeholder">Nome do cartão</string>
|
||||
<string name="tangempay_card_details_reveal_text">Revelar</string>
|
||||
<string name="tangempay_card_details_show_details">Mostrar detalhes</string>
|
||||
<string name="tangempay_card_details_swap_description">Troque qualquer ativo da sua carteira por um cartão.</string>
|
||||
<string name="tangempay_card_details_title">Detalhes do cartão</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Por favor, tente novamente mais tarde.</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Descongelar cartão</string>
|
||||
|
|
@ -1766,6 +1772,7 @@
|
|||
<string name="tangempay_card_page_daily_limit_change">Mudar</string>
|
||||
<string name="tangempay_card_page_daily_limit_current_limit">Limite atual</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_description">Não foi possível carregar seu limite diário. Tente novamente.</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_subtitle">Recarregue a página para tentar novamente.</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_title">Limite diário indisponível</string>
|
||||
<string name="tangempay_card_page_daily_limit_success_description">Você pode alterar isso novamente quando quiser.</string>
|
||||
<string name="tangempay_card_page_daily_limit_success_title">O limite diário está definido.</string>
|
||||
|
|
@ -1827,7 +1834,7 @@
|
|||
<string name="tangempay_onboarding_security_title">Privacidade incomparável</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_description">E vincule um cartão de pagamento a ele.</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">Vamos configurar uma carteira.</string>
|
||||
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay gratuito em minutos.</string>
|
||||
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay em minutos</string>
|
||||
<string name="tangempay_pay_support">Suporte de Pay</string>
|
||||
<string name="tangempay_payment_account">Conta de pagamento</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay sessão expirada</string>
|
||||
|
|
@ -1851,7 +1858,6 @@
|
|||
<string name="tangempay_sync_needed">Use o cartão ou anel para renovar a sessão</string>
|
||||
<string name="tangempay_sync_needed_body">Use o cartão ou anel para renovar a sessão</string>
|
||||
<string name="tangempay_sync_needed_button">Restaurar acesso</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Restaurar acesso</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay sessão expirada</string>
|
||||
<string name="tangempay_tangem_visa_card">Use USDC para pagamentos do dia a dia.</string>
|
||||
<string name="tangempay_temporarily_unavailable">O serviço Tangem Pay está temporariamente inacessível.</string>
|
||||
|
|
@ -1861,7 +1867,6 @@
|
|||
<string name="tangempay_topup_swap_body">Troque qualquer ativo por USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">Da sua Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC na rede Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Clique no botão abaixo para restaurar o acesso.</string>
|
||||
<string name="tangempay_withdrawal_note_description">Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras</string>
|
||||
<string name="tangempay_withdrawal_note_title">Observe</string>
|
||||
<string name="tangempay_your_pin_code">Seu código PIN</string>
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
<string name="common_accounts">Аккаунты</string>
|
||||
<string name="common_activate">Активировать</string>
|
||||
<string name="common_add">Добавить</string>
|
||||
<string name="common_add_funds">Добавить средств</string>
|
||||
<string name="common_add_funds">Пополнить</string>
|
||||
<string name="common_add_to_portfolio">Добавить в портфель</string>
|
||||
<string name="common_add_token">Добавить токен</string>
|
||||
<string name="common_add_tokens">Добавьте токены</string>
|
||||
|
|
@ -410,6 +410,7 @@
|
|||
<string name="common_select_action">Выберите действие</string>
|
||||
<string name="common_sell">Продать</string>
|
||||
<string name="common_send">Отправить</string>
|
||||
<string name="common_send_colon">Отправка:</string>
|
||||
<string name="common_send_tx_error">Не удалось отправить транзакцию</string>
|
||||
<string name="common_server_unavailable">Сервер недоступен, повторите попытку позднее</string>
|
||||
<string name="common_share">Поделиться</string>
|
||||
|
|
@ -782,6 +783,8 @@
|
|||
<string name="koinos_mana_level_description">Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Уровень маны</string>
|
||||
<string name="main_add_and_manage_tokens">Добавить и управлять</string>
|
||||
<string name="main_add_funds_promo_description">Купите криптовалюту или переведите её на свой кошелёк.</string>
|
||||
<string name="main_add_funds_promo_title">Пополните кошелёк</string>
|
||||
<string name="main_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string>
|
||||
<string name="main_manage_tokens">Управление токенами</string>
|
||||
<string name="main_qr_scan_hint">Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению.</string>
|
||||
|
|
@ -1684,15 +1687,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Тарифы и полные условия</string>
|
||||
<string name="tangem_pay_terms_limits">Тарифы и лимиты</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">Банк отклонил транзакцию</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Эта комиссия покрывает стоимость обработки вашего перевода.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Комиссия взимается в соответствии с тарифами обслуживания</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">Транзакция частично или полностью возвращена продавцом</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Продолжайте пользоваться картой, заморозить всегда успеете</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Разморозить карту?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Не удалось разморозить карту, попробуйте еще раз</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Карта разморожена</string>
|
||||
<string name="tangem_pay_withdrawal">Вывод средств</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Это произошло из-за регуляторных требований. Вывод средств по-прежнему доступен.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Карта была деактивирована</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">По вопросам данных или истории транзакций, обратитесь в поддержку</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Аккаунт закрыт</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Запрещено использовать на root-устройствах</string>
|
||||
<string name="tangempay_available_balance">Баланс</string>
|
||||
<string name="tangempay_cancel_kyc">Скрыть KYC с главной</string>
|
||||
|
|
@ -1726,7 +1729,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Добавьте карту в Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Добавить карту в Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">ПИН-код</string>
|
||||
<string name="tangempay_card_details_receive_description">Скопируйте свой адрес или покажите QR</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Техническая ошибка. Попробуйте позже или обратитесь в поддержку.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Пополнение недоступно</string>
|
||||
<string name="tangempay_card_details_reissue_card">Перевыпустить карту</string>
|
||||
|
|
@ -1734,7 +1736,6 @@
|
|||
<string name="tangempay_card_details_rename_card_invalid_title">Недопустимые символы</string>
|
||||
<string name="tangempay_card_details_reveal_text">Показать</string>
|
||||
<string name="tangempay_card_details_show_details">Реквизиты</string>
|
||||
<string name="tangempay_card_details_swap_description">Пополните карту любым активом через обмен </string>
|
||||
<string name="tangempay_card_details_title">Реквизиты</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Пожалуйста, попробуйте позже</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Разморозить карту</string>
|
||||
|
|
@ -1816,7 +1817,6 @@
|
|||
<string name="tangempay_sync_needed">Используйте карту или кольцо для обновления сессии</string>
|
||||
<string name="tangempay_sync_needed_body">Используйте карту или кольцо для обновления сессии</string>
|
||||
<string name="tangempay_sync_needed_button">Обновить сессию</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Обновить сессию</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay · Cессия истекла</string>
|
||||
<string name="tangempay_tangem_visa_card">Оплачивайте ежедневные покупки в USDC</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay временно недоступен</string>
|
||||
|
|
@ -1826,7 +1826,6 @@
|
|||
<string name="tangempay_topup_swap_body">Обменяйте любой актив на USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">Из вашего кошелька Tangem</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC в сети Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Нажмите на кнопку ниже, чтобы восстановить доступ</string>
|
||||
<string name="tangempay_withdrawal_note_description">При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок</string>
|
||||
<string name="tangempay_withdrawal_note_title">Обратите внимание</string>
|
||||
<string name="tangempay_your_pin_code">Ваш PIN-код</string>
|
||||
|
|
@ -2071,6 +2070,7 @@
|
|||
<string name="warning_express_notification_invalid_reserve_amount_title">Сумма получения не может быть менее %s</string>
|
||||
<string name="warning_express_pair_unavailable_message">Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s)</string>
|
||||
<string name="warning_express_pair_unavailable_title">Выбранная пара временно недоступна</string>
|
||||
<string name="warning_express_providers_fca_warning_description">Для пользователей из Великобритании: некоторые провайдеры не авторизованы FCA Великобритании. Вам следует избегать взаимодействия с ними.</string>
|
||||
<string name="warning_express_providers_fca_warning_title">Предупреждающий список FCA</string>
|
||||
<string name="warning_express_refresh_required_title">Сервис временно недоступен</string>
|
||||
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@
|
|||
<string name="common_select_action">Оберіть дію</string>
|
||||
<string name="common_sell">Продати</string>
|
||||
<string name="common_send">Надіслати</string>
|
||||
<string name="common_send_colon">Відправка:</string>
|
||||
<string name="common_send_tx_error">Не вдалося надіслати транзакцію</string>
|
||||
<string name="common_server_unavailable">Сервер недоступний, спробуйте пізніше</string>
|
||||
<string name="common_share">Поширити</string>
|
||||
|
|
@ -1604,15 +1605,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Умови, комісії та ліміти</string>
|
||||
<string name="tangem_pay_terms_limits">Умови та обмеження</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">Банк відхилив цей запит на транзакцію.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Ця комісія покриває витрати на обробку вашого переказу.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">Комісія стягується відповідно до тарифів обслуговування</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">Транзакцію було частково або повністю скасовано продавцем</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">Продовжуйте користуватися карткою. Заморозити можна в будь-який момент.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Розморозити картку?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Не вдалося розморозити картку. Спробуйте пізніше.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Картку розморожено.</string>
|
||||
<string name="tangem_pay_withdrawal">Виведення коштів</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">Це було зроблено відповідно до регуляторних вимог. Виведення коштів усе ще доступне.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Вашу картку було деактивовано</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">З питань щодо даних або історії транзакцій зверніться до служби підтримки</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Ваш обліковий запис було закрито</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Заборонено використовувати на root-пристроях</string>
|
||||
<string name="tangempay_available_balance">Баланс</string>
|
||||
<string name="tangempay_cancel_kyc">Приховати KYC з головного екрана</string>
|
||||
|
|
@ -1644,7 +1645,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">Додайте картку до Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">Додайте свою картку в Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">ПІН-код</string>
|
||||
<string name="tangempay_card_details_receive_description">Поділіться своєю адресою або покажіть QR-код</string>
|
||||
<string name="tangempay_card_details_receive_error_description">Виявлено технічні проблеми. Будь ласка, спробуйте пізніше або зверніться до служби підтримки.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Поповнення наразі недоступне</string>
|
||||
<string name="tangempay_card_details_reissue_card">Перевипустити картку</string>
|
||||
|
|
@ -1652,7 +1652,6 @@
|
|||
<string name="tangempay_card_details_rename_card_invalid_title">Неприпустимі символи</string>
|
||||
<string name="tangempay_card_details_reveal_text">Показати</string>
|
||||
<string name="tangempay_card_details_show_details">Показати деталі</string>
|
||||
<string name="tangempay_card_details_swap_description">Обміняйте будь-який актив у вашому портфелі на картку</string>
|
||||
<string name="tangempay_card_details_title">Реквізити картки</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Будь ласка, спробуйте пізніше</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Розморозити картку</string>
|
||||
|
|
@ -1713,7 +1712,7 @@
|
|||
<string name="tangempay_onboarding_purchases_title">Платіть стільки, скільки бачите</string>
|
||||
<string name="tangempay_onboarding_security_description">Буде створено окремий платіжний рахунок без розкриття ваших адрес та активів</string>
|
||||
<string name="tangempay_onboarding_security_title">Неперевершена конфіденційність</string>
|
||||
<string name="tangempay_onboarding_title">Отримайте безкоштовну картку Tangem Pay за лічені хвилини</string>
|
||||
<string name="tangempay_onboarding_title">Отримайте картку Tangem Pay за лічені хвилини</string>
|
||||
<string name="tangempay_payment_account">Платіжний акаунт</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay · Сесія закінчилася</string>
|
||||
<string name="tangempay_pin_validation_error_message">Слабкий ПІН: не використовуйте повторів або послідовностей.</string>
|
||||
|
|
@ -1735,7 +1734,6 @@
|
|||
<string name="tangempay_sync_needed">Використайте картку або кільце для поновлення сесії</string>
|
||||
<string name="tangempay_sync_needed_body">Використайте картку або кільце для поновлення сесії</string>
|
||||
<string name="tangempay_sync_needed_button">Відновити доступ</string>
|
||||
<string name="tangempay_sync_needed_restore_access">Відновити доступ</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay · Сесія закінчилася</string>
|
||||
<string name="tangempay_tangem_visa_card">Використовуйте USDC для щоденних платежів</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay тимчасово недоступний</string>
|
||||
|
|
@ -1745,7 +1743,6 @@
|
|||
<string name="tangempay_topup_swap_body">Обміняйте будь-який актив на USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">З вашого Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC у Polygon</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Натисніть кнопку нижче, щоб відновити доступ</string>
|
||||
<string name="tangempay_withdrawal_note_description">Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок.</string>
|
||||
<string name="tangempay_withdrawal_note_title">Зверніть увагу</string>
|
||||
<string name="tangempay_your_pin_code">Ваш PIN-код</string>
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@
|
|||
<string name="common_sending">发送中</string>
|
||||
<string name="common_sent">发送</string>
|
||||
<string name="common_server_unavailable">服务器不可用,请稍后再试。</string>
|
||||
<string name="common_session_expired">会话已过期</string>
|
||||
<string name="common_share">分享</string>
|
||||
<string name="common_share_link">分享链接</string>
|
||||
<string name="common_show_less">显示更少</string>
|
||||
|
|
@ -1187,9 +1188,7 @@
|
|||
<string name="organize_tokens_title">整理代币</string>
|
||||
<string name="organize_tokens_ungroup">取消分组</string>
|
||||
<string name="provider_name_support">%s 支持</string>
|
||||
<string name="push_notification_settings_banner_button_grant_permission">授予权限</string>
|
||||
<string name="push_notification_settings_banner_description">推送通知已启用,但需要您在设备设置中允许通知才能正常工作。</string>
|
||||
<string name="push_notification_settings_banner_description_grant_permission">推送通知已启用,但需要您授予权限才能生效。</string>
|
||||
<string name="push_notification_settings_banner_title">允许通知</string>
|
||||
<string name="push_notification_settings_offers_updates_subtitle">产品资讯、独家优惠和活动提醒。</string>
|
||||
<string name="push_notification_settings_offers_updates_title">优惠与更新</string>
|
||||
|
|
@ -1671,15 +1670,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">条款、费用和限制</string>
|
||||
<string name="tangem_pay_terms_limits">条款和限制</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">银行拒绝了这项交易请求。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">这笔费用用于支付您办理转账时的费用。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">费用按服务费率收取</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">商家部分或全部撤销了交易</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">继续使用您的资金。您可以随时冻结资金。</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">要解冻您的卡片?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">卡片解冻失败,请稍后再试。</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">您的卡片已解冻。</string>
|
||||
<string name="tangem_pay_withdrawal">提款</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">这是根据监管要求执行的。不过,提现仍然可用。</string>
|
||||
<string name="tangempay_account_deactivated_message_title">您的卡已停用</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">如需咨询账户、数据或交易记录,请联系支持团队</string>
|
||||
<string name="tangempay_account_deactivated_message_title">您的账户已被关闭</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">无法在已root的设备上使用</string>
|
||||
<string name="tangempay_available_balance">可用余额</string>
|
||||
<string name="tangempay_cancel_kyc">从主屏幕隐藏 KYC 页面</string>
|
||||
|
|
@ -1713,7 +1712,6 @@
|
|||
<string name="tangempay_card_details_open_wallet_title">将卡片添加到 Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">将卡片添加到 Apple Pay</string>
|
||||
<string name="tangempay_card_details_pin_code">PIN码</string>
|
||||
<string name="tangempay_card_details_receive_description">分享您的地址或出示二维码</string>
|
||||
<string name="tangempay_card_details_receive_error_description">检测到技术问题。请稍后再试或联系技术支持。</string>
|
||||
<string name="tangempay_card_details_receive_error_title">目前无法接收</string>
|
||||
<string name="tangempay_card_details_reissue_card">重新发行卡片</string>
|
||||
|
|
@ -1722,7 +1720,6 @@
|
|||
<string name="tangempay_card_details_rename_card_placeholder">卡片名称</string>
|
||||
<string name="tangempay_card_details_reveal_text">显示</string>
|
||||
<string name="tangempay_card_details_show_details">显示详情</string>
|
||||
<string name="tangempay_card_details_swap_description">将您投资组合中的任何资产互换到卡片</string>
|
||||
<string name="tangempay_card_details_title">卡片详情</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">请稍后再试。</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">解冻卡片</string>
|
||||
|
|
@ -1800,7 +1797,7 @@
|
|||
<string name="tangempay_onboarding_security_title">无与伦比的隐私保护</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_description">并将其与支付卡关联。</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">我们将设置一个钱包。</string>
|
||||
<string name="tangempay_onboarding_title">几分钟内即可获得免费的 Tangem Pay 卡</string>
|
||||
<string name="tangempay_onboarding_title">立即获取你的 Tangem Pay 卡</string>
|
||||
<string name="tangempay_pay_support">支付支持</string>
|
||||
<string name="tangempay_payment_account">支付账户</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay 会话已过期</string>
|
||||
|
|
@ -1824,7 +1821,6 @@
|
|||
<string name="tangempay_sync_needed">用卡或戒指续期会话</string>
|
||||
<string name="tangempay_sync_needed_body">用卡或戒指续期会话</string>
|
||||
<string name="tangempay_sync_needed_button">恢复访问权限</string>
|
||||
<string name="tangempay_sync_needed_restore_access">恢复访问权限</string>
|
||||
<string name="tangempay_sync_needed_title">Tangem Pay 会话已过期</string>
|
||||
<string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay暂时无法使用。</string>
|
||||
|
|
@ -1834,7 +1830,6 @@
|
|||
<string name="tangempay_topup_swap_body">將任何資產兌換為 USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">從您的 Tangem 錢包</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">Polygon网络上的 USDC</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">点击下方按钮恢复访问权限</string>
|
||||
<string name="tangempay_withdrawal_note_description">您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。</string>
|
||||
<string name="tangempay_withdrawal_note_title">请注意</string>
|
||||
<string name="tangempay_your_pin_code">您的PIN码</string>
|
||||
|
|
|
|||
|
|
@ -337,15 +337,15 @@
|
|||
<string name="tangem_pay_terms_fees_limits">條款、費用與限制</string>
|
||||
<string name="tangem_pay_terms_limits">條款與限制</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">銀行拒絕了此交易請求。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">此費用用於支付處理您轉帳的成本。</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">費用依服務費率收取</string>
|
||||
<string name="tangem_pay_transaction_reversed_notification_text">該交易已被商家部分或全額撤銷</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_body">繼續使用您的資金。您可以隨時凍結。</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">解凍您的卡片?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">無法解凍卡片。請稍後再試。</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">您的卡片已解凍。</string>
|
||||
<string name="tangem_pay_withdrawal">提現</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">这是根据监管要求执行的。不过,提现仍然可用。</string>
|
||||
<string name="tangempay_account_deactivated_message_title">您的卡已停用</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">如需查詢帳戶、資料或交易記錄,請聯絡客服支援</string>
|
||||
<string name="tangempay_account_deactivated_message_title">您的帳戶已被關閉</string>
|
||||
<string name="tangempay_cancel_kyc">在主畫面隱藏身份驗證</string>
|
||||
<string name="tangempay_card_details_add_funds">添加资金</string>
|
||||
<string name="tangempay_card_details_add_funds_subtitle">充值选项</string>
|
||||
|
|
@ -374,12 +374,10 @@
|
|||
<string name="tangempay_card_details_open_wallet_step_5">全部完成!您的卡片已準備就緒。</string>
|
||||
<string name="tangempay_card_details_open_wallet_title">將卡片添加到 Google Pay</string>
|
||||
<string name="tangempay_card_details_open_wallet_title_apple">添加卡片到 Apple Pay</string>
|
||||
<string name="tangempay_card_details_receive_description">分享您的地址或显示二维码</string>
|
||||
<string name="tangempay_card_details_receive_error_title">暫時無法接收</string>
|
||||
<string name="tangempay_card_details_reissue_card">重新发行卡片</string>
|
||||
<string name="tangempay_card_details_reveal_text">显示</string>
|
||||
<string name="tangempay_card_details_show_details">顯示詳情</string>
|
||||
<string name="tangempay_card_details_swap_description">將您投資組合中的任何資產兌換成卡片</string>
|
||||
<string name="tangempay_card_details_title">卡片详情</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">解凍卡片</string>
|
||||
<string name="tangempay_card_details_withdraw">提现</string>
|
||||
|
|
@ -420,7 +418,7 @@
|
|||
<string name="tangempay_onboarding_purchases_title">所見即所付</string>
|
||||
<string name="tangempay_onboarding_security_description">將創建單獨的支付帳戶,且不會透露您的地址和資產</string>
|
||||
<string name="tangempay_onboarding_security_title">無與倫比的隱私</string>
|
||||
<string name="tangempay_onboarding_title">在幾分鐘內獲得免費的 Tangem Pay 卡</string>
|
||||
<string name="tangempay_onboarding_title">立即獲取你的 Tangem Pay 卡</string>
|
||||
<string name="tangempay_payment_account">付款帳戶</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Tangem Pay 工作階段已過期</string>
|
||||
<string name="tangempay_reissue_card_description">這將產生一組新的卡片資料。您的舊資料將停止使用。此操作無法復原。</string>
|
||||
|
|
@ -439,7 +437,6 @@
|
|||
<string name="tangempay_topup_receive_title">从其他钱包或交易所</string>
|
||||
<string name="tangempay_topup_swap_body">将任何资产兑换为 USDC Polygon</string>
|
||||
<string name="tangempay_topup_swap_title">从您的 Tangem 钱包</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">點擊下方按鈕以恢復存取權限</string>
|
||||
<string name="tangempay_withdrawal_note_description">您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。</string>
|
||||
<string name="tangempay_withdrawal_note_title">請注意</string>
|
||||
<string name="tangempay_your_pin_code">您的PIN码</string>
|
||||
|
|
|
|||
|
|
@ -664,6 +664,11 @@
|
|||
<string name="feedback_subject_support_tangem">Tangem feedback</string>
|
||||
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
|
||||
<string name="feedback_token_description_error">Coin description error</string>
|
||||
<string name="force_update_banner_message">Update the application to the latest version to ensure proper functionality</string>
|
||||
<string name="force_update_banner_title">Update Needed</string>
|
||||
<string name="force_update_button">Update</string>
|
||||
<string name="force_update_warning_message">Please update the application to the latest version to ensure proper functionality.</string>
|
||||
<string name="force_update_warning_title">Update Required</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Not enough funds</string>
|
||||
<string name="gasless_transaction_fee">Transaction fee</string>
|
||||
<string name="generic_error">An error occurred</string>
|
||||
|
|
@ -789,8 +794,8 @@
|
|||
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Mana level</string>
|
||||
<string name="main_add_and_manage_tokens">Add & Manage</string>
|
||||
<string name="main_add_funds_promo_description">Deposit crypto or buy with card to get started</string>
|
||||
<string name="main_add_funds_promo_title">Add funds to start earning and trading</string>
|
||||
<string name="main_add_funds_promo_description">Buy or receive crypto to start using your wallet.</string>
|
||||
<string name="main_add_funds_promo_title">Get your first crypto</string>
|
||||
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
|
||||
<string name="main_manage_tokens">Manage tokens</string>
|
||||
<string name="main_qr_scan_hint">Scan QR code to send funds or connect to an app</string>
|
||||
|
|
@ -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_frozen">Card frozen</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_secondary_button_title">Go back</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_other">Other</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_status_completed">Completed</string>
|
||||
<string name="tangem_pay_status_declined">Declined</string>
|
||||
|
|
@ -1701,15 +1707,17 @@
|
|||
<string name="tangem_pay_terms_fees_limits">Terms, Fees & Limits</string>
|
||||
<string name="tangem_pay_terms_limits">Terms and fees</string>
|
||||
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
|
||||
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
|
||||
<string name="tangem_pay_transaction_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_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
|
||||
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
|
||||
<string name="tangem_pay_withdrawal">Withdrawal</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">This was done due to regulatory requirements. Anyway withdrawals are still available.</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Your card was deactivated</string>
|
||||
<string name="tangempay_account_deactivated_message_subtitle">For questions about account, data or transaction history, please contact support</string>
|
||||
<string name="tangempay_account_deactivated_message_title">Your account has been closed</string>
|
||||
<string name="tangempay_account_unable_to_use_rooted">Unable to use on rooted device</string>
|
||||
<string name="tangempay_available_balance">Available balance</string>
|
||||
<string name="tangempay_cancel_kyc">Hide KYC from main screen</string>
|
||||
|
|
@ -1819,6 +1827,24 @@
|
|||
<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_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_title">Use USDC for everyday payments</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_setup_wallet_description">And link a payment card to it</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
|
||||
<string name="tangempay_onboarding_title">Get your free Tangem Pay Card in minutes</string>
|
||||
<string name="tangempay_onboarding_title">Get your Tangem Pay Card in minutes</string>
|
||||
<string name="tangempay_pay_support">Pay Support</string>
|
||||
<string name="tangempay_payment_account">Payment account</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Payment account session expired</string>
|
||||
|
|
@ -1847,16 +1873,16 @@
|
|||
<string name="tangempay_reissue_card_title">Replace your card?</string>
|
||||
<string name="tangempay_service_unavailable_description">We’re fixing a technical issue. Please try again later.</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_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_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_button">Renew session</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_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_topup_receive_body">Send USDC Polygon to your account’s address </string>
|
||||
<string name="tangempay_topup_receive_title">From another wallet or exchange</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.core.ui.components.provider
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.domain.express.models.ProviderFilterType
|
||||
|
|
@ -20,31 +20,31 @@ fun ProviderTypeFilterPicker(
|
|||
onFilterSelect: (ProviderFilterType) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val segments = availableFilters.map { filter ->
|
||||
TangemSegmentUM(
|
||||
id = filter.name,
|
||||
title = when (filter) {
|
||||
ProviderFilterType.ALL -> resourceReference(R.string.common_all)
|
||||
ProviderFilterType.CEX -> TextReference.Str("CEX")
|
||||
ProviderFilterType.DEX -> TextReference.Str("DEX")
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
val selectedSegment = segments.firstOrNull { it.id == selectedFilter.name }
|
||||
TangemThemeRedesign {
|
||||
// key() forces recomposition when selectedFilter changes to re-seed initialSelectedItem,
|
||||
// because TangemSegmentedPicker owns its selection state internally via remember.
|
||||
key(selectedFilter) {
|
||||
TangemSegmentedPicker(
|
||||
items = segments,
|
||||
initialSelectedItem = selectedSegment,
|
||||
isFixed = true,
|
||||
modifier = modifier,
|
||||
onClick = { segment ->
|
||||
val filterType = availableFilters.firstOrNull { it.name == segment.id }
|
||||
if (filterType != null) onFilterSelect(filterType)
|
||||
val segments = remember(availableFilters) {
|
||||
availableFilters.map { filter ->
|
||||
TangemSegmentUM(
|
||||
id = filter.name,
|
||||
title = when (filter) {
|
||||
ProviderFilterType.ALL -> resourceReference(R.string.common_all)
|
||||
ProviderFilterType.CEX -> TextReference.Str("CEX")
|
||||
ProviderFilterType.DEX -> TextReference.Str("DEX")
|
||||
},
|
||||
)
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
val selectedSegment = remember(segments, selectedFilter) {
|
||||
segments.firstOrNull { it.id == selectedFilter.name }
|
||||
}
|
||||
TangemThemeRedesign {
|
||||
TangemSegmentedPicker(
|
||||
items = segments,
|
||||
initialSelectedItem = selectedSegment,
|
||||
isFixed = true,
|
||||
modifier = modifier,
|
||||
onClick = { segment ->
|
||||
val filterType = availableFilters.firstOrNull { it.name == segment.id }
|
||||
if (filterType != null) onFilterSelect(filterType)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,14 @@
|
|||
package com.tangem.core.ui.ds2.surface
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material3.LocalRippleConfiguration
|
||||
import androidx.compose.material3.RippleConfiguration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
|
@ -94,6 +87,7 @@ fun TangemSurface(
|
|||
onClick = onClick!!,
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -464,6 +464,10 @@ val LocalRedesignEnabled = staticCompositionLocalOf<Boolean> {
|
|||
false
|
||||
}
|
||||
|
||||
val LocalVisaRedesignEnabled = staticCompositionLocalOf<Boolean> {
|
||||
false
|
||||
}
|
||||
|
||||
val LocalPowerSavingState = compositionLocalOf<PowerSavingState> {
|
||||
error("No PowerSavingState provided")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -15,12 +16,13 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Coroutines
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
api(deps.kotlin.serialization)
|
||||
// endregion
|
||||
|
||||
// region Time dependencies
|
||||
implementation(deps.jodatime)
|
||||
api(deps.jodatime)
|
||||
// endregion
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
|||
|
||||
// region Project - Domain
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.dynamicAddresses)
|
||||
implementation(projects.domain.dynamicAddresses.models)
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncOrNull
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
|
|
@ -7,6 +9,7 @@ import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
|||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
|
@ -21,11 +24,22 @@ class DynamicAddressesInitializer @Inject constructor(
|
|||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) {
|
||||
|
||||
suspend fun getXpubs(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, String> {
|
||||
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap()
|
||||
|
||||
/*
|
||||
* Dynamic addresses rely on the server-side wallet accounts list, which is populated only for
|
||||
* multi-currency wallets. Single-currency wallets (Note, s2c, etc.) never populate it, so
|
||||
* DynamicAddressesRepository.getStatus() — backed by WalletAccountsFetcher.get() — would never
|
||||
* emit and firstOrNull() below would suspend forever, hanging the whole balance fetch and leaving
|
||||
* the currency stuck in Loading. Skip such wallets entirely. ([REDACTED_TASK_KEY])
|
||||
*/
|
||||
val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId)
|
||||
if (userWallet == null || !userWallet.isMultiCurrency) return emptyMap()
|
||||
|
||||
val result = mutableMapOf<Network, String>()
|
||||
for (network in networks) {
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.Either.Companion.catch
|
||||
import com.tangem.blockchain.blockchains.ethereum.Chain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
|
||||
|
||||
@Deprecated("Use TangemPayCurrencyFactory instead")
|
||||
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val errorConverter: TangemPayErrorConverter,
|
||||
) : TangemPayCryptoCurrencyFactory {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
NetworkFactory(excludedBlockchains)
|
||||
}
|
||||
|
||||
override fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency> {
|
||||
return catch {
|
||||
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
|
||||
val blockchain = requireNotNull(chain.blockchain)
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID),
|
||||
name = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||
)
|
||||
}.mapLeft { exception ->
|
||||
TangemLogger.withTag(TAG).e("Error", exception)
|
||||
errorConverter.convert(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
|
|
@ -11,7 +10,9 @@ import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
|||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
fiatRate = value.fiatRate,
|
||||
cards = value.cards.map { card ->
|
||||
PaymentAccountStatusValueDM.TangemPayCard(
|
||||
id = card.id,
|
||||
|
|
@ -52,7 +54,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
adminDailyLimit = card.limit?.adminCardLimit?.amount,
|
||||
frozenState = card.frozenState.toString(),
|
||||
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.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
|
||||
fiatRate = value.fiatRate,
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
)
|
||||
|
|
@ -93,6 +96,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = value.fiatRate,
|
||||
cards = value.cards.map { card ->
|
||||
TangemPayCard(
|
||||
id = card.id,
|
||||
|
|
@ -108,7 +112,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
),
|
||||
frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
|
||||
lastDigits = card.lastDigits,
|
||||
isReissuing = card.isReissuing,
|
||||
state = TangemPayCardState.fromString(card.state),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -122,6 +126,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = value.fiatRate,
|
||||
)
|
||||
null -> PaymentAccountStatusValue.Error.Unavailable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import androidx.datastore.core.DataStoreFactory
|
|||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||
import com.tangem.data.pay.entity.DefaultTangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
|
||||
import com.tangem.data.pay.repository.*
|
||||
|
|
@ -21,19 +21,13 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
|||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
|
||||
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
|
||||
import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase
|
||||
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
|
||||
import com.tangem.domain.pay.usecase.*
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
|
|
@ -75,9 +69,11 @@ internal interface TangemPayDataModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCryptoCurrencyFactory(
|
||||
factory: DefaultTangemPayCryptoCurrencyFactory,
|
||||
): TangemPayCryptoCurrencyFactory
|
||||
fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
@ -220,5 +216,20 @@ internal interface TangemPayDataModule {
|
|||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCloseTangemPayCardUseCase(
|
||||
closeCardRepository: TangemPayCloseCardRepository,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CloseTangemPayCardUseCase {
|
||||
return CloseTangemPayCardUseCase(
|
||||
closeCardRepository = closeCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,20 +8,21 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|||
import com.tangem.domain.common.wallets.requireUserWalletsSync
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TangemPayCurrencyFactory @Inject constructor(
|
||||
internal class DefaultTangemPayCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
) : TangemPayCurrencyFactory {
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
|
||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||
override fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||
val userWallet = userWalletsListRepository.requireUserWalletsSync()
|
||||
.firstOrNull { it.walletId == userWalletId }
|
||||
?: error("User wallet with id $userWalletId not found")
|
||||
|
|
@ -32,18 +33,11 @@ internal class TangemPayCurrencyFactory @Inject constructor(
|
|||
)
|
||||
return cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
rawId = TangemPayCurrencyFactory.TOKEN_ID,
|
||||
name = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val TOKEN_ID = "usd-coin"
|
||||
internal const val TOKEN_NAME = "USDC"
|
||||
internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
internal const val TOKEN_DECIMALS = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.data.pay.flow
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -11,7 +10,9 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue
|
|||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
|
|
@ -21,9 +22,14 @@ import com.tangem.domain.pay.model.TangemPayEntryPoint
|
|||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.pay.model.isFinalStatus
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -31,12 +37,13 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
private const val TAG = "PaymentAccountStatusFetcher"
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -46,6 +53,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
private val reissueCardRepository: TangemPayReissueCardRepository,
|
||||
private val singleQuoteSupplier: SingleQuoteStatusSupplier,
|
||||
private val closeCardRepository: TangemPayCloseCardRepository,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
|
|
@ -259,6 +268,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
val quotesData = singleQuoteSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID),
|
||||
)?.value as? QuoteStatus.Data
|
||||
val cardInfo = this.cardInfo
|
||||
val productInstance = this.productInstance
|
||||
|
||||
|
|
@ -281,12 +293,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
)
|
||||
}
|
||||
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
productInstance = productInstance,
|
||||
cardInfo = cardInfo,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
|
|
@ -298,17 +312,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
productInstance: CustomerInfo.ProductInstance,
|
||||
cardInfo: CustomerInfo.CardInfo,
|
||||
customerId: String,
|
||||
fiatRate: BigDecimal?,
|
||||
): PaymentAccountStatusValue {
|
||||
val reissueOrder = reissueCardRepository.getReissueOrderInfo(
|
||||
userWalletId = userWalletId,
|
||||
cardId = productInstance.cardId,
|
||||
).getOrNull()
|
||||
|
||||
val isReissuing = reissueOrder != null &&
|
||||
reissueOrder.orderStatus != OrderStatus.CANCELED &&
|
||||
reissueOrder.orderStatus != OrderStatus.COMPLETED
|
||||
|
||||
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(productInstance.cardId)
|
||||
val cardId = productInstance.cardId
|
||||
val cardState = getCardState(cardId, userWalletId)
|
||||
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(cardId)
|
||||
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
|
|
@ -319,9 +327,10 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
availableForWithdrawal = cardInfo.availableForWithdrawal,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = fiatRate,
|
||||
cards = listOf(
|
||||
TangemPayCard(
|
||||
id = productInstance.cardId,
|
||||
id = cardId,
|
||||
hasPinCode = cardInfo.isPinSet,
|
||||
displayName = productInstance.displayName,
|
||||
limit = TangemPayCardLimitData(
|
||||
|
|
@ -334,12 +343,35 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
productInstance.frozenState
|
||||
},
|
||||
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 {
|
||||
return when (this) {
|
||||
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.datasource.local.visa.TangemPayReissueCardStore
|
|||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
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.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
|
|
@ -21,7 +20,6 @@ internal class DefaultReissueCardRepository @Inject constructor(
|
|||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : TangemPayReissueCardRepository {
|
||||
|
||||
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
|
||||
|
|
@ -74,17 +72,11 @@ internal class DefaultReissueCardRepository @Inject constructor(
|
|||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getReissueOrderInfo(
|
||||
override suspend fun getReissueOrderId(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayOrderInfo?> = either {
|
||||
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
|
||||
|
||||
if (orderId == null) {
|
||||
return null.right()
|
||||
}
|
||||
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
|
||||
): Either<UniversalError, String?> = either {
|
||||
runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Nested
|
||||
|
|
@ -70,6 +70,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
|
|||
),
|
||||
cryptoBalance = cryptoBalance(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = BigDecimal("1.05"),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -80,6 +81,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
|
|||
val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount
|
||||
assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100"))
|
||||
assertThat(dm.fiatBalance.currency).isEqualTo("USD")
|
||||
assertThat(dm.fiatRate).isEqualTo(BigDecimal("1.05"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -144,6 +146,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
|
|||
currency = "EUR",
|
||||
),
|
||||
cryptoBalance = cryptoBalanceDM(),
|
||||
fiatRate = BigDecimal("0.92"),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -155,6 +158,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
|
|||
assertThat(deactivated.source).isEqualTo(StatusSource.CACHE)
|
||||
assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200"))
|
||||
assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR")
|
||||
assertThat(deactivated.fiatRate).isEqualTo(BigDecimal("0.92"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -16,43 +16,26 @@ internal object YieldBoostStatusConverter {
|
|||
private const val REASON_CLOSED = "closed"
|
||||
|
||||
fun convert(dto: YieldBoostStatusResponse): YieldBoostStatus = when (dto.promoEnrollmentStatus.lowercase()) {
|
||||
STATUS_ACTIVE -> dto.toActive() ?: YieldBoostStatus.NotStarted
|
||||
STATUS_COMPLETED -> dto.toCompleted() ?: YieldBoostStatus.NotStarted
|
||||
STATUS_ACTIVE, STATUS_COMPLETED -> dto.toEnrolled()
|
||||
STATUS_DISQUALIFIED -> YieldBoostStatus.Disqualified(reason = dto.disqualificationReason.toReason())
|
||||
STATUS_NOT_STARTED -> YieldBoostStatus.NotStarted
|
||||
else -> YieldBoostStatus.NotStarted // forward-compat: unknown status → treat as NotStarted
|
||||
}
|
||||
|
||||
/** Backend `"active"` → [YieldBoostStatus.Active]. Returns `null` if mandatory dates can't be parsed. */
|
||||
private fun YieldBoostStatusResponse.toActive(): YieldBoostStatus.Active? {
|
||||
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
|
||||
val qualificationEnd =
|
||||
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
|
||||
return YieldBoostStatus.Active(
|
||||
tokenName = tokenName.orEmpty(),
|
||||
networkId = networkId.orEmpty(),
|
||||
moduleAddress = moduleAddress.orEmpty(),
|
||||
userAddress = userAddress.orEmpty(),
|
||||
contractAddress = contractAddress.orEmpty(),
|
||||
activationDate = activation,
|
||||
qualificationEndDate = qualificationEnd,
|
||||
)
|
||||
}
|
||||
|
||||
private fun YieldBoostStatusResponse.toCompleted(): YieldBoostStatus.Completed? {
|
||||
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
|
||||
val qualificationEnd =
|
||||
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
|
||||
return YieldBoostStatus.Completed(
|
||||
tokenName = tokenName.orEmpty(),
|
||||
networkId = networkId.orEmpty(),
|
||||
moduleAddress = moduleAddress.orEmpty(),
|
||||
userAddress = userAddress.orEmpty(),
|
||||
contractAddress = contractAddress.orEmpty(),
|
||||
activationDate = activation,
|
||||
qualificationEndDate = qualificationEnd,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Backend `"active"` / `"completed"` → [YieldBoostStatus.Enrolled].
|
||||
*
|
||||
* An unparseable / missing `qualificationEndDate` is kept as `null` (block hidden) — never downgraded to
|
||||
* [YieldBoostStatus.NotStarted], which would re-prompt an already-enrolled user to join.
|
||||
*/
|
||||
private fun YieldBoostStatusResponse.toEnrolled(): YieldBoostStatus.Enrolled = YieldBoostStatus.Enrolled(
|
||||
tokenName = tokenName.orEmpty(),
|
||||
networkId = networkId.orEmpty(),
|
||||
moduleAddress = moduleAddress.orEmpty(),
|
||||
userAddress = userAddress.orEmpty(),
|
||||
contractAddress = contractAddress.orEmpty(),
|
||||
qualificationEndDate = qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() },
|
||||
)
|
||||
|
||||
private fun String?.toReason(): YieldBoostStatus.Disqualified.Reason = when (this?.lowercase()) {
|
||||
REASON_FROD -> YieldBoostStatus.Disqualified.Reason.FROD
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ package com.tangem.data.yield.supply.promo.converter
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
|
||||
import com.tangem.domain.yield.supply.models.YieldBoostStatus
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class YieldBoostStatusConverterTest {
|
||||
|
||||
private val activation = "2026-05-01T00:00:00Z"
|
||||
private val qualificationEnd = "2026-06-01T00:00:00Z"
|
||||
|
||||
@Test
|
||||
|
|
@ -20,7 +20,7 @@ class YieldBoostStatusConverterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN active backend status with valid dates WHEN convert THEN returns Active`() {
|
||||
fun `GIVEN active backend status with valid date WHEN convert THEN returns Enrolled`() {
|
||||
val dto = dto(
|
||||
promoEnrollmentStatus = "active",
|
||||
tokenName = "USD Coin",
|
||||
|
|
@ -28,47 +28,49 @@ class YieldBoostStatusConverterTest {
|
|||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = "0xcontract",
|
||||
activationDate = activation,
|
||||
qualificationEndDate = qualificationEnd,
|
||||
)
|
||||
|
||||
val result = YieldBoostStatusConverter.convert(dto)
|
||||
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java)
|
||||
val active = result as YieldBoostStatus.Active
|
||||
assertThat(active.tokenName).isEqualTo("USD Coin")
|
||||
assertThat(active.networkId).isEqualTo("ethereum")
|
||||
assertThat(active.contractAddress).isEqualTo("0xcontract")
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
|
||||
val enrolled = result as YieldBoostStatus.Enrolled
|
||||
assertThat(enrolled.tokenName).isEqualTo("USD Coin")
|
||||
assertThat(enrolled.networkId).isEqualTo("ethereum")
|
||||
assertThat(enrolled.contractAddress).isEqualTo("0xcontract")
|
||||
assertThat(enrolled.qualificationEndDate).isEqualTo(Instant.parse(qualificationEnd))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN active status missing activationDate WHEN convert THEN falls back to NotStarted`() {
|
||||
fun `GIVEN active status missing qualificationEndDate WHEN convert THEN returns Enrolled with null date`() {
|
||||
val dto = dto(
|
||||
promoEnrollmentStatus = "active",
|
||||
activationDate = null,
|
||||
qualificationEndDate = qualificationEnd,
|
||||
contractAddress = "0xcontract",
|
||||
qualificationEndDate = null,
|
||||
)
|
||||
|
||||
val result = YieldBoostStatusConverter.convert(dto)
|
||||
|
||||
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
|
||||
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN active status with malformed activationDate WHEN convert THEN falls back to NotStarted`() {
|
||||
fun `GIVEN active status with malformed qualificationEndDate WHEN convert THEN returns Enrolled with null date`() {
|
||||
val dto = dto(
|
||||
promoEnrollmentStatus = "active",
|
||||
activationDate = "not-an-iso",
|
||||
qualificationEndDate = qualificationEnd,
|
||||
contractAddress = "0xcontract",
|
||||
qualificationEndDate = "not-an-iso",
|
||||
)
|
||||
|
||||
val result = YieldBoostStatusConverter.convert(dto)
|
||||
|
||||
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
|
||||
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN completed status with valid dates WHEN convert THEN returns Completed`() {
|
||||
fun `GIVEN completed status with valid date WHEN convert THEN returns Enrolled`() {
|
||||
val dto = dto(
|
||||
promoEnrollmentStatus = "completed",
|
||||
tokenName = "USDT",
|
||||
|
|
@ -76,13 +78,14 @@ class YieldBoostStatusConverterTest {
|
|||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = "0xcontract",
|
||||
activationDate = "2026-04-01T00:00:00Z",
|
||||
qualificationEndDate = "2026-05-01T00:00:00Z",
|
||||
)
|
||||
|
||||
val result = YieldBoostStatusConverter.convert(dto)
|
||||
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Completed::class.java)
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
|
||||
assertThat((result as YieldBoostStatus.Enrolled).qualificationEndDate)
|
||||
.isEqualTo(Instant.parse("2026-05-01T00:00:00Z"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -153,13 +156,12 @@ class YieldBoostStatusConverterTest {
|
|||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = "0xcontract",
|
||||
activationDate = activation,
|
||||
qualificationEndDate = qualificationEnd,
|
||||
)
|
||||
|
||||
val result = YieldBoostStatusConverter.convert(dto)
|
||||
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java)
|
||||
assertThat(result).isInstanceOf(YieldBoostStatus.Enrolled::class.java)
|
||||
}
|
||||
|
||||
private fun dto(
|
||||
|
|
@ -169,7 +171,6 @@ class YieldBoostStatusConverterTest {
|
|||
moduleAddress: String? = null,
|
||||
userAddress: String? = null,
|
||||
contractAddress: String? = null,
|
||||
activationDate: String? = null,
|
||||
qualificationEndDate: String? = null,
|
||||
disqualificationReason: String? = null,
|
||||
) = YieldBoostStatusResponse(
|
||||
|
|
@ -179,7 +180,6 @@ class YieldBoostStatusConverterTest {
|
|||
userAddress = userAddress,
|
||||
contractAddress = contractAddress,
|
||||
promoEnrollmentStatus = promoEnrollmentStatus,
|
||||
activationDate = activationDate,
|
||||
qualificationEndDate = qualificationEndDate,
|
||||
disqualificationReason = disqualificationReason,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,14 @@ sealed class PaymentAccountStatusValue {
|
|||
is UnderReview,
|
||||
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
|
||||
is Loading -> TotalFiatBalance.Loading
|
||||
is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
is Deactivated -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
is Loaded -> {
|
||||
val rate = this.fiatRate ?: return TotalFiatBalance.Failed
|
||||
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
|
||||
}
|
||||
is Deactivated -> {
|
||||
val rate = this.fiatRate ?: return TotalFiatBalance.Failed
|
||||
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,6 +105,11 @@ sealed class PaymentAccountStatusValue {
|
|||
*
|
||||
* @property source The source of the status information.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
* @property cryptoCurrency The crypto currency held by the deactivated account.
|
||||
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
|
||||
* or `null` if the quote is not yet available. When `null`,
|
||||
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
||||
*/
|
||||
@Serializable
|
||||
data class Deactivated(
|
||||
|
|
@ -106,25 +117,15 @@ sealed class PaymentAccountStatusValue {
|
|||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val fiatRate: SerializedBigDecimal?,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
value = buildCryptoCurrencyStatusValue(
|
||||
amount = cryptoBalance.balance,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = cryptoBalance.depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
fiatRate = fiatRate,
|
||||
depositAddress = cryptoBalance.depositAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -139,7 +140,11 @@ sealed class PaymentAccountStatusValue {
|
|||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
|
||||
* @property cryptoCurrency The crypto currency held by the account.
|
||||
* @property cards The list of user's cards.
|
||||
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
|
||||
* or `null` if the quote is not yet available. When `null`,
|
||||
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
||||
*/
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
|
|
@ -152,25 +157,15 @@ sealed class PaymentAccountStatusValue {
|
|||
val availableForWithdrawal: SerializedBigDecimal,
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val cards: List<TangemPayCard>,
|
||||
val fiatRate: SerializedBigDecimal?,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
value = buildCryptoCurrencyStatusValue(
|
||||
amount = availableForWithdrawal,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = cryptoBalance.depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
fiatRate = fiatRate,
|
||||
depositAddress = cryptoBalance.depositAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -235,6 +230,44 @@ sealed class PaymentAccountStatusValue {
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildCryptoCurrencyStatusValue(
|
||||
amount: SerializedBigDecimal,
|
||||
fiatAmount: SerializedBigDecimal,
|
||||
fiatRate: SerializedBigDecimal?,
|
||||
depositAddress: String,
|
||||
): CryptoCurrencyStatus.Value {
|
||||
val networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = depositAddress,
|
||||
),
|
||||
)
|
||||
return if (fiatRate != null) {
|
||||
CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatRate = fiatRate,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = networkAddress,
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyStatus.NoQuote(
|
||||
amount = amount,
|
||||
networkAddress = networkAddress,
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
|
||||
|
||||
fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId }
|
||||
|
|
|
|||
|
|
@ -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 frozenState whether the card is currently frozen (blocked for payments).
|
||||
* @property lastDigits The last four digits of the card number.
|
||||
* @property state current lifecycle state of the card.
|
||||
*/
|
||||
@Serializable
|
||||
data class TangemPayCard(
|
||||
|
|
@ -22,7 +23,7 @@ data class TangemPayCard(
|
|||
@SerialName("limit") val limit: TangemPayCardLimitData?,
|
||||
@SerialName("frozen_state") val frozenState: TangemPayCardFrozenState,
|
||||
@SerialName("last_digits") val lastDigits: String,
|
||||
@SerialName("is_reissuing") val isReissuing: Boolean,
|
||||
@SerialName("state") val state: TangemPayCardState,
|
||||
)
|
||||
|
||||
val TangemPayCard.isFrozen
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,9 @@ class BalanceFetchingOperations(
|
|||
async {
|
||||
val result = when (source) {
|
||||
FetchingSource.NETWORK -> fetchNetworks(userWalletId, currencies)
|
||||
FetchingSource.QUOTE -> fetchQuotes(currencies)
|
||||
FetchingSource.QUOTE -> fetchQuotes(
|
||||
currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
|
||||
)
|
||||
FetchingSource.STAKING -> fetchStaking(userWalletId, currencies)
|
||||
}
|
||||
source to result
|
||||
|
|
@ -85,17 +87,14 @@ class BalanceFetchingOperations(
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetches quotes for the given currencies.
|
||||
* Fetches quotes for the given raw currency ids.
|
||||
*
|
||||
* @param currencies the cryptocurrencies to fetch quotes for
|
||||
* @param rawCurrencyIds the raw currency ids to fetch quotes for
|
||||
* @return Either with Unit on success or Throwable on failure
|
||||
*/
|
||||
suspend fun fetchQuotes(currencies: Collection<CryptoCurrency>): Either<Throwable, Unit> {
|
||||
suspend fun fetchQuotes(rawCurrencyIds: Set<CryptoCurrency.RawID>): Either<Throwable, Unit> {
|
||||
return multiQuoteStatusFetcher(
|
||||
params = MultiQuoteStatusFetcher.Params(
|
||||
currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
|
||||
appCurrencyId = null,
|
||||
),
|
||||
params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrencyIds, appCurrencyId = null),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
|
|
@ -173,6 +174,7 @@ class WalletBalanceFetcher internal constructor(
|
|||
|
||||
// Fetch TangemPay separately — may run long-polling, so it must not block balance error checking
|
||||
if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) {
|
||||
balanceFetchingOperations.fetchQuotes(rawCurrencyIds = setOf(TangemPayCurrencyFactory.TOKEN_ID))
|
||||
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.domain.pay
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
@Deprecated("TangemPayCurrencyFactory")
|
||||
interface TangemPayCryptoCurrencyFactory {
|
||||
|
||||
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.pay
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Factory that builds the [CryptoCurrency.Token] used by Tangem Pay (USDC on Polygon) for a given user wallet.
|
||||
*
|
||||
* Replaces the deprecated `TangemPayCryptoCurrencyFactory`: callers no longer pass the chain id explicitly —
|
||||
* the underlying network is resolved from the wallet.
|
||||
*/
|
||||
interface TangemPayCurrencyFactory {
|
||||
|
||||
/**
|
||||
* Builds the Tangem Pay token bound to the network of the wallet identified by [userWalletId].
|
||||
*
|
||||
* @throws IllegalStateException if no wallet with [userWalletId] is currently loaded.
|
||||
*/
|
||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token
|
||||
|
||||
/** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */
|
||||
companion object {
|
||||
/** CoinGecko-style raw id used to query quotes for the Tangem Pay token. */
|
||||
val TOKEN_ID = CryptoCurrency.RawID("usd-coin")
|
||||
const val TOKEN_NAME = "USDC"
|
||||
const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
const val TOKEN_DECIMALS = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -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?>
|
||||
}
|
||||
|
|
@ -15,8 +15,5 @@ interface TangemPayReissueCardRepository {
|
|||
|
||||
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun getReissueOrderInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayOrderInfo?>
|
||||
suspend fun getReissueOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,26 +6,22 @@ sealed interface YieldBoostStatus {
|
|||
|
||||
data object NotStarted : YieldBoostStatus
|
||||
|
||||
/** User entered boost, qualification period is still running. */
|
||||
data class Active(
|
||||
/**
|
||||
* User is enrolled in the boost (backend `active` or `completed`).
|
||||
*
|
||||
* The boost block on the active screen is driven entirely by [qualificationEndDate], which the backend
|
||||
* computes as the end of the bonus-accrual period:
|
||||
* - `null` — nothing is shown;
|
||||
* - in the future — days left until the date;
|
||||
* - reached / passed — awaiting payout.
|
||||
*/
|
||||
data class Enrolled(
|
||||
val tokenName: String,
|
||||
val networkId: String,
|
||||
val moduleAddress: String,
|
||||
val userAddress: String,
|
||||
val contractAddress: String,
|
||||
val activationDate: Instant,
|
||||
val qualificationEndDate: Instant,
|
||||
) : YieldBoostStatus
|
||||
|
||||
/** Boost has finished (backend `completed`). */
|
||||
data class Completed(
|
||||
val tokenName: String,
|
||||
val networkId: String,
|
||||
val moduleAddress: String,
|
||||
val userAddress: String,
|
||||
val contractAddress: String,
|
||||
val activationDate: Instant,
|
||||
val qualificationEndDate: Instant,
|
||||
val qualificationEndDate: Instant?,
|
||||
) : YieldBoostStatus
|
||||
|
||||
data class Disqualified(val reason: Reason) : YieldBoostStatus {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.math.BigInteger
|
|||
import java.math.RoundingMode
|
||||
|
||||
private val HUNDRED_PERCENT = 100.toBigInteger() // base 100%
|
||||
val INCREASE_GAS_LIMIT_FOR_SUPPLY = 120.toBigInteger() // 20% increase
|
||||
val INCREASE_GAS_LIMIT_FOR_SUPPLY = 140.toBigInteger() // 20% increase
|
||||
|
||||
fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = when (this) {
|
||||
is Fee.Ethereum.Legacy -> copy(
|
||||
|
|
|
|||
|
|
@ -316,9 +316,9 @@ class YieldSupplyEstimateEnterFeeUseCaseTest {
|
|||
val deployFee = txs.first().fee as Fee.Ethereum.EIP1559
|
||||
val approveFee = txs[1].fee as Fee.Ethereum.EIP1559
|
||||
val enterFee = txs.last().fee as Fee.Ethereum.EIP1559
|
||||
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_200))
|
||||
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_400))
|
||||
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_600))
|
||||
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_400))
|
||||
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_800))
|
||||
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(4_200))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -355,9 +355,9 @@ class YieldSupplyEstimateEnterFeeUseCaseTest {
|
|||
val deployFee = txs.first().fee as Fee.Ethereum.Legacy
|
||||
val approveFee = txs[1].fee as Fee.Ethereum.Legacy
|
||||
val enterFee = txs.last().fee as Fee.Ethereum.Legacy
|
||||
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_200))
|
||||
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_400))
|
||||
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_600))
|
||||
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_400))
|
||||
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_800))
|
||||
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(4_200))
|
||||
}
|
||||
|
||||
private fun getDeployTx() = uncompiled(
|
||||
|
|
|
|||
|
|
@ -91,21 +91,10 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest {
|
||||
fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest {
|
||||
val token = createToken()
|
||||
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
|
||||
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus()
|
||||
|
||||
val result = useCase(userWalletId, token)
|
||||
|
||||
assertThat(result.getOrNull()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN status is Completed WHEN invoke THEN returns Right(false)`() = runTest {
|
||||
val token = createToken()
|
||||
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
|
||||
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns completedStatus()
|
||||
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus()
|
||||
|
||||
val result = useCase(userWalletId, token)
|
||||
|
||||
|
|
@ -162,26 +151,15 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest {
|
|||
link = null,
|
||||
)
|
||||
|
||||
private fun activeStatus() = YieldBoostStatus.Active(
|
||||
private fun enrolledStatus() = YieldBoostStatus.Enrolled(
|
||||
tokenName = "USD Coin",
|
||||
networkId = networkRawId,
|
||||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = contractAddress,
|
||||
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
|
||||
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
|
||||
)
|
||||
|
||||
private fun completedStatus() = YieldBoostStatus.Completed(
|
||||
tokenName = "USD Coin",
|
||||
networkId = networkRawId,
|
||||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = contractAddress,
|
||||
activationDate = Instant.parse("2026-04-01T00:00:00Z"),
|
||||
qualificationEndDate = Instant.parse("2026-05-01T00:00:00Z"),
|
||||
)
|
||||
|
||||
private fun createToken(
|
||||
contractAddress: String = this.contractAddress,
|
||||
networkRawId: String = this.networkRawId,
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ class ShouldShowYieldBoostMainBannerUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest {
|
||||
fun `GIVEN status is Enrolled WHEN invoke THEN returns Right(false)`() = runTest {
|
||||
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
|
||||
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus()
|
||||
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns enrolledStatus()
|
||||
|
||||
val result = useCase(userWalletId)
|
||||
|
||||
|
|
@ -92,13 +92,12 @@ class ShouldShowYieldBoostMainBannerUseCaseTest {
|
|||
link = null,
|
||||
)
|
||||
|
||||
private fun activeStatus() = YieldBoostStatus.Active(
|
||||
private fun enrolledStatus() = YieldBoostStatus.Enrolled(
|
||||
tokenName = "USD Coin",
|
||||
networkId = networkRawId,
|
||||
moduleAddress = "0xmodule",
|
||||
userAddress = "0xuser",
|
||||
contractAddress = contractAddress,
|
||||
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
|
||||
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
|
||||
)
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ interface SelectApprovalTypeComponent : ComposableBottomSheetComponent {
|
|||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val amountFooter: TextReference,
|
||||
val initialApproveType: ApproveType = ApproveType.LIMITED,
|
||||
val spenderAddress: String,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onApproveTypeSelected(approveType: ApproveType)
|
||||
fun onApproveTypeSelected(spenderAddress: String, approveType: ApproveType)
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal class SelectApprovalTypeModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onConfirmClick() {
|
||||
params.callback.onApproveTypeSelected(uiState.value.approveType)
|
||||
params.callback.onApproveTypeSelected(params.spenderAddress, uiState.value.approveType)
|
||||
}
|
||||
|
||||
fun onCancelClick() {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,29 @@
|
|||
package com.tangem.features.commonfeatures.api.addfunds
|
||||
|
||||
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
|
||||
|
||||
interface AddFundsComponent : ComposableContentComponent {
|
||||
interface AddFundsComponent : ComposableBottomSheetComponent {
|
||||
|
||||
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>
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
|
||||
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
|
@ -94,7 +95,14 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
|
|||
val wallet: UserWallet,
|
||||
val account: AccountStatus.CryptoPortfolio,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal {
|
|||
val title: TextReference,
|
||||
val isShowMarketBlock: Boolean,
|
||||
val isShowPaymentAccount: Boolean,
|
||||
val isAppBarShown: Boolean = true,
|
||||
) {
|
||||
companion object {
|
||||
val SwapFrom = Settings(
|
||||
|
|
@ -45,6 +46,7 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal {
|
|||
title = resourceReference(R.string.swapping_to_title),
|
||||
isShowMarketBlock = true,
|
||||
isShowPaymentAccount = false,
|
||||
isAppBarShown = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +92,11 @@ data class ChooseTokenResult(
|
|||
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
|
||||
) {
|
||||
val walletId get() = wallet.walletId
|
||||
|
||||
val wasJustAdded: Boolean
|
||||
get() = analyticsPayload
|
||||
.filterIsInstance<ChooseTokenAnalyticsPayload.IsMarketTokenSelected>()
|
||||
.any { it.value }
|
||||
}
|
||||
|
||||
sealed interface ChooseTokenAnalyticsPayload {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.features.commonfeatures.api.tokenactions
|
||||
|
||||
enum class BottomAction { GoToToken, None }
|
||||
|
|
@ -1,93 +1,232 @@
|
|||
package com.tangem.features.commonfeatures.impl.addfunds
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
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.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
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.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
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.TangemThemeRedesign
|
||||
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
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.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultAddFundsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: AddFundsComponent.Params,
|
||||
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
|
||||
tokenActionsComponentFactory: TokenActionsComponent.Factory,
|
||||
userPortfolioComponentFactory: UserPortfolioComponent.Factory,
|
||||
walletFeatureToggles: WalletFeatureToggles,
|
||||
) : AppComponentContext by appComponentContext, AddFundsComponent {
|
||||
|
||||
private val model: AddFundsModel = getOrCreateModel(params)
|
||||
|
||||
private val chooseTokenComponent: ChooseTokenComponent = chooseTokenComponentFactory.create(
|
||||
context = child(key = "addFundsChooseToken"),
|
||||
params = ChooseTokenComponent.Params(bridge = model.chooseTokenBridge),
|
||||
)
|
||||
private val isCompactTokenActions: Boolean = params.launchMode is AddFundsComponent.LaunchMode.TokenActionsOnly
|
||||
|
||||
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create(
|
||||
context = child(key = "addFundsTokenActions"),
|
||||
params = TokenActionsComponent.Params(
|
||||
data = model.tokenActionsData,
|
||||
callbacks = model,
|
||||
bottomAction = TokenActionsComponent.BottomAction.GoToToken,
|
||||
isRedesignForced = true,
|
||||
),
|
||||
)
|
||||
private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled
|
||||
|
||||
private val tokenActionsComponent: TokenActionsComponent by lazy {
|
||||
tokenActionsComponentFactory.create(
|
||||
context = child(key = "addFundsTokenActions"),
|
||||
params = TokenActionsComponent.Params(
|
||||
data = model.tokenActionsData,
|
||||
callbacks = model,
|
||||
bottomAction = model.currentBottomAction,
|
||||
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
|
||||
override fun Content(modifier: Modifier) {
|
||||
chooseTokenComponent.Content(modifier)
|
||||
val isTokenActionsShown by model.isTokenActionsShown.collectAsStateWithLifecycle()
|
||||
if (isTokenActionsShown) {
|
||||
// force use redesign theme here according to the task requirements, will be reworked in the next release
|
||||
TangemThemeRedesign {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = model::onTokenActionsDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
containerColor = TangemTheme.colors2.surface.level2,
|
||||
scrollableContent = true,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
title = resourceReference(R.string.common_get_token),
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = model::onTokenActionsDismiss,
|
||||
)
|
||||
},
|
||||
content = { _ ->
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens2.x4,
|
||||
top = TangemTheme.dimens2.x2,
|
||||
end = TangemTheme.dimens2.x4,
|
||||
bottom = TangemTheme.dimens2.x4,
|
||||
),
|
||||
) {
|
||||
tokenActionsComponent.Content(Modifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
override fun BottomSheet() {
|
||||
val route by model.uiRoute.collectAsStateWithLifecycle()
|
||||
val canGoBack by model.canGoBack.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(route) {
|
||||
if (route != AddFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect
|
||||
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(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
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,
|
||||
title = {
|
||||
AddFundsBottomSheetTitle(
|
||||
route = route,
|
||||
canGoBack = canGoBack,
|
||||
onBackClick = model::onBack,
|
||||
onCloseClick = ::dismiss,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
val animatedContentModifier =
|
||||
if (params.launchMode is AddFundsComponent.LaunchMode.ChooseToken) {
|
||||
Modifier.fillMaxSize()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
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,
|
||||
) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ internal sealed class AddFundsAnalyticsEvent(
|
|||
|
||||
class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive")
|
||||
|
||||
class ButtonGoToToken : AddFundsAnalyticsEvent(event = "Button - Go to Token")
|
||||
|
||||
companion object {
|
||||
private const val CATEGORY = "Add Funds"
|
||||
const val SOURCE_MAIN_SCREEN = "Main Screen"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue