Updated on 2026-08-14
This commit is contained in:
commit
d0b35bc331
680 changed files with 26556 additions and 2709 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
name: analyze-logs
|
name: analyze-logs
|
||||||
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
|
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
|
||||||
allowed-tools: Read, Grep
|
allowed-tools: Read, Grep
|
||||||
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf]
|
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] [--no-secrets-audit]
|
||||||
---
|
---
|
||||||
|
|
||||||
Analyze the Tangem app user log file at path: `$ARGUMENTS`
|
Analyze the Tangem app user log file at path: `$ARGUMENTS`
|
||||||
|
|
@ -108,12 +108,37 @@ Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (dev
|
||||||
- `MainActivity.*onNewIntent` — deep link or push notification
|
- `MainActivity.*onNewIntent` — deep link or push notification
|
||||||
- `CardSDK_Session.*start card session` — NFC session starts
|
- `CardSDK_Session.*start card session` — NFC session starts
|
||||||
|
|
||||||
|
**Secrets & PII Audit (full file, head_limit: 20 each, -n: true):**
|
||||||
|
|
||||||
|
Skip this entire group if `--no-secrets-audit` is in arguments.
|
||||||
|
|
||||||
|
- API key leak in URL: `[?&](api[_-]?key|apiKey|access_token|token|secret)=(?!\*+)[^&\s]{8,}`
|
||||||
|
- Bearer token: `Bearer\s+[A-Za-z0-9._\-]{20,}`
|
||||||
|
- JWT: `eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+`
|
||||||
|
- Authorization header: `(?i)authorization:\s*\S+`
|
||||||
|
- Critical PII in JSON: `"(privateKey|mnemonic|seedPhrase|private_key|seed_phrase)"\s*:\s*"[^"]+"`
|
||||||
|
- card_public_key in JSON: `"card_public_key"\s*:\s*"[^"]{40,}"`
|
||||||
|
- FCM push token: `:APA91[A-Za-z0-9_\-]{100,}`
|
||||||
|
- xprv/tprv extended private key: `\b[xytzuv]prv[A-Za-z0-9]{100,}`
|
||||||
|
- Suspicious long hex in URL path: `https?://[^?\s]+/[A-Fa-f0-9]{32,}\b`
|
||||||
|
- Masking health check: count of `\*{6,}` — if 0 in a build that should mask, flag pipeline broken
|
||||||
|
|
||||||
**Error filtering:** When processing error results, skip these noisy matches:
|
**Error filtering:** When processing error results, skip these noisy matches:
|
||||||
- `java.io.IOException: Canceled` — normal request cancellation
|
- `java.io.IOException: Canceled` — normal request cancellation
|
||||||
- `HttpException(code=304` — HTTP "Not Modified"
|
- `HttpException(code=304` — HTTP "Not Modified"
|
||||||
- Bare stacktrace lines starting with `\tat`
|
- Bare stacktrace lines starting with `\tat`
|
||||||
- `<-- HTTP FAILED: java.io.IOException: Canceled`
|
- `<-- HTTP FAILED: java.io.IOException: Canceled`
|
||||||
|
|
||||||
|
### Step 6.5: Masking Consistency Check
|
||||||
|
|
||||||
|
Skip if `--no-secrets-audit` in arguments. Run sequentially after the parallel batch (needs results from the masked-endpoint grep).
|
||||||
|
|
||||||
|
1. Grep `https?://[^/\s]+/[^\s*]*\*{6,}` (full file, head_limit: 50) — collect all URLs where a path segment is masked
|
||||||
|
2. For each unique `host + path-prefix-before-mask`, derive the prefix string
|
||||||
|
3. For each prefix, Grep the prefix followed by a non-`*` character (`<prefix>[^*\s]`, head_limit: 20)
|
||||||
|
- If hits found → masking inconsistency: same endpoint has both masked and unmasked variants
|
||||||
|
- Record the prefix, count of masked hits, count of unmasked hits, first unmasked line number
|
||||||
|
|
||||||
### Step 7: Deep Dive
|
### Step 7: Deep Dive
|
||||||
|
|
||||||
For each significant error found above:
|
For each significant error found above:
|
||||||
|
|
@ -207,6 +232,37 @@ Structure your report EXACTLY as follows:
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
(chronological: app starts, card sessions, navigation, errors, notable API calls)
|
(chronological: app starts, card sessions, navigation, errors, notable API calls)
|
||||||
|
|
||||||
|
## Secrets & PII Audit
|
||||||
|
|
||||||
|
Omit this section entirely if `--no-secrets-audit` was passed.
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
- Total masked tokens (`******`) in log: **N**
|
||||||
|
- If N = 0 in a build expected to mask, flag: "masking pipeline may be broken"
|
||||||
|
|
||||||
|
### Confirmed Leaks (CRITICAL / HIGH)
|
||||||
|
| Line | Severity | Type | Matched (first 16 chars + `…`) | Context |
|
||||||
|
|------|----------|------|--------------------------------|---------|
|
||||||
|
|
||||||
|
### Masking Inconsistencies
|
||||||
|
| Endpoint Prefix | Masked Hits | Unmasked Hits | First Unmasked Line |
|
||||||
|
|-----------------|-------------|---------------|---------------------|
|
||||||
|
|
||||||
|
### Suspected Leaks (MEDIUM / LOW)
|
||||||
|
| Line | Severity | Type | Pattern Matched | Why Suspect |
|
||||||
|
|------|----------|------|-----------------|-------------|
|
||||||
|
|
||||||
|
**Severity legend:**
|
||||||
|
- **CRITICAL** — private key / mnemonic / xprv in clear text
|
||||||
|
- **HIGH** — API key / bearer / JWT / card_public_key visible
|
||||||
|
- **MEDIUM** — push token, card_id, persistent identifiers
|
||||||
|
- **LOW** — heuristic patterns that may be false positives (tx hash, content hash)
|
||||||
|
|
||||||
|
**Output rules:**
|
||||||
|
- Never include the full matched value — always truncate to 16 chars + `…`
|
||||||
|
- For LOW severity, add a "Why Suspect" column explaining typical false positives
|
||||||
|
- Skip matches from these known-public Tangem endpoints: `/v1/coins/settings`, `/v1/geo`, `/v1/currencies`, `/v1/hot_crypto`
|
||||||
|
|
||||||
## Analysis Summary
|
## Analysis Summary
|
||||||
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
|
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
|
||||||
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
|
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
||||||
|
},
|
||||||
|
"notion": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +283,8 @@ dependencies {
|
||||||
implementation(projects.features.onboardingV2.impl)
|
implementation(projects.features.onboardingV2.impl)
|
||||||
implementation(projects.features.stories.api)
|
implementation(projects.features.stories.api)
|
||||||
implementation(projects.features.stories.impl)
|
implementation(projects.features.stories.impl)
|
||||||
|
implementation(projects.features.survey.api)
|
||||||
|
implementation(projects.features.survey.impl)
|
||||||
implementation(projects.features.txhistory.api)
|
implementation(projects.features.txhistory.api)
|
||||||
implementation(projects.features.txhistory.impl)
|
implementation(projects.features.txhistory.impl)
|
||||||
implementation(projects.features.biometry.api)
|
implementation(projects.features.biometry.api)
|
||||||
|
|
|
||||||
|
|
@ -183,9 +183,11 @@ abstract class BaseTestCase : TestCase(
|
||||||
"GASLESS_APPROVAL_ENABLED" to true,
|
"GASLESS_APPROVAL_ENABLED" to true,
|
||||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||||
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
||||||
|
"ASSETS_DISCOVERY_ENABLED" to true,
|
||||||
"VISA_ONBOARDING_ENABLED" to true,
|
"VISA_ONBOARDING_ENABLED" to true,
|
||||||
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
||||||
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
||||||
|
"APP_REDESIGN_ENABLED" to true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ object TestConstants {
|
||||||
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
||||||
const val REFERRAL_API_SCENARIO = "referral_api"
|
const val REFERRAL_API_SCENARIO = "referral_api"
|
||||||
const val QUOTES_API_SCENARIO = "quotes_api"
|
const val QUOTES_API_SCENARIO = "quotes_api"
|
||||||
|
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"
|
||||||
|
const val WALLET_TOKENS_API_SCENARIO = "wallet_tokens_api"
|
||||||
|
const val MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO = "moralis_evm_token_balances_api"
|
||||||
|
const val PROVIDERS_API_SCENARIO = "networks_providers"
|
||||||
|
|
||||||
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
|
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
|
||||||
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
|
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
|
||||||
|
|
@ -60,6 +64,9 @@ object TestConstants {
|
||||||
"bread much nature basic fun iron benefit egg error prosper"
|
"bread much nature basic fun iron benefit egg error prosper"
|
||||||
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
|
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
|
||||||
|
|
||||||
|
const val SEED_PHRASE_HAPPY_PATH =
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
|
||||||
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
||||||
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
||||||
}
|
}
|
||||||
|
|
@ -115,5 +115,13 @@ private fun extractText(node: SemanticsNode): String? {
|
||||||
|
|
||||||
private fun parseVolume(node: SemanticsNode): Double? {
|
private fun parseVolume(node: SemanticsNode): Double? {
|
||||||
val text = extractText(node) ?: return null
|
val text = extractText(node) ?: return null
|
||||||
return text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull()
|
val multiplier = when {
|
||||||
|
text.contains('T', ignoreCase = true) -> 1_000_000_000_000.0
|
||||||
|
text.contains('B', ignoreCase = true) -> 1_000_000_000.0
|
||||||
|
text.contains('M', ignoreCase = true) -> 1_000_000.0
|
||||||
|
text.contains('K', ignoreCase = true) -> 1_000.0
|
||||||
|
else -> 1.0
|
||||||
|
}
|
||||||
|
val number = text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull() ?: return null
|
||||||
|
return number * multiplier
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,11 @@ fun KNode.clickWithAssertion() {
|
||||||
performClick()
|
performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun KNode.clickWhenEnabled() {
|
||||||
|
assertIsEnabled()
|
||||||
|
performClick()
|
||||||
|
}
|
||||||
|
|
||||||
fun KNode.assertTextContainsSafe(
|
fun KNode.assertTextContainsSafe(
|
||||||
text: String,
|
text: String,
|
||||||
substring: Boolean = false,
|
substring: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import androidx.test.uiautomator.By
|
||||||
import androidx.test.uiautomator.Until
|
import androidx.test.uiautomator.Until
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.wallet.R
|
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
|
||||||
|
|
||||||
fun BaseTestCase.swipeVertical(
|
fun BaseTestCase.swipeVertical(
|
||||||
direction: SwipeDirection,
|
direction: SwipeDirection,
|
||||||
|
|
@ -31,21 +29,6 @@ fun BaseTestCase.pullToRefresh(steps: Int = 1000) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
|
|
||||||
val searchBarText = device.uiDevice
|
|
||||||
.findObject(By.textContains(getResourceString(R.string.markets_search_header_title)))
|
|
||||||
val bounds = searchBarText.visibleBounds
|
|
||||||
|
|
||||||
val centerX = bounds.centerX()
|
|
||||||
val startY = bounds.centerY()
|
|
||||||
val endY = when (direction) {
|
|
||||||
SwipeDirection.UP -> 50
|
|
||||||
SwipeDirection.DOWN -> device.uiDevice.displayHeight - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
device.uiDevice.swipe(centerX, startY, centerX, endY, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BaseTestCase.openTheAppFromRecents() {
|
fun BaseTestCase.openTheAppFromRecents() {
|
||||||
device.uiDevice.waitForIdle()
|
device.uiDevice.waitForIdle()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,96 +6,35 @@ import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.checkSingleCurrencyMainScreen(
|
fun BaseTestCase.checkSingleCurrencyMainScreen(cardTitle: String) {
|
||||||
cardBlockchain: String,
|
|
||||||
cardTitle: String,
|
|
||||||
withTransactions: Boolean = false,
|
|
||||||
withWalletImage: Boolean = true
|
|
||||||
) {
|
|
||||||
step("Assert card title equal '$cardTitle'") {
|
step("Assert card title equal '$cardTitle'") {
|
||||||
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
||||||
}
|
}
|
||||||
if (withWalletImage) {
|
|
||||||
step("Assert card image is displayed") { //TODO: create assertion method for checking images
|
|
||||||
onMainScreen { walletImage.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert card image is not displayed") {
|
|
||||||
onMainScreen { walletImage.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert 'Receive' button is displayed") {
|
|
||||||
onMainScreen { receiveButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Buy' button is displayed") {
|
step("Assert 'Buy' button is displayed") {
|
||||||
onMainScreen { buyButton.assertIsDisplayed() }
|
onMainScreen { buyButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is displayed") {
|
|
||||||
onMainScreen { sendButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Sell' button is displayed") {
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
onMainScreen { sellButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is not displayed") {
|
step("Assert 'Swap' button is not displayed") {
|
||||||
onMainScreen { swapButton.assertIsNotDisplayed() }
|
onMainScreen { swapButton.assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Market Price' on single card main screen is displayed") {
|
|
||||||
onMainScreen { marketPriceBlock().assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Market Price' title equals $cardBlockchain Market Price") {
|
|
||||||
onMainScreen { marketPriceText.assertTextContains("$cardBlockchain Market Price") }
|
|
||||||
}
|
|
||||||
step("Swipe up") {
|
step("Swipe up") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
swipeVertical(SwipeDirection.UP)
|
||||||
}
|
}
|
||||||
if (withTransactions) {
|
|
||||||
step("Assert 'Transactions' block is displayed") {
|
|
||||||
onMainScreen { transactionsExplorerText.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Transactions' title is displayed") {
|
|
||||||
onMainScreen { transactionsTitle.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Explorer' icon is displayed") {
|
|
||||||
onMainScreen { transactionsExplorerIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert empty 'Transactions' block is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlock.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block icon is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block text is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockText.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block 'Explore' button is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockExploreButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert 'Add & Manage' button is not displayed") {
|
step("Assert 'Add & Manage' button is not displayed") {
|
||||||
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
|
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.checkMultiCurrencyMainScreen(
|
fun BaseTestCase.checkMultiCurrencyMainScreen(
|
||||||
devicesCount: String,
|
|
||||||
cardTitle: String,
|
cardTitle: String,
|
||||||
withWalletImage: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
step("Assert card title equal '$cardTitle'") {
|
step("Assert card title equal '$cardTitle'") {
|
||||||
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
||||||
}
|
}
|
||||||
if (withWalletImage) {
|
step("Assert 'Buy' button is displayed") {
|
||||||
step("Assert card image is displayed") {
|
onMainScreen { buyButton.assertIsDisplayed() }
|
||||||
onMainScreen { walletImage.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert card image is not displayed") {
|
|
||||||
onMainScreen { walletImage.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert devices count equal to '$devicesCount'") {
|
|
||||||
onMainScreen { walletDevicesCount.assertTextContains(devicesCount) }
|
|
||||||
}
|
}
|
||||||
step("Assert 'Add funds' button is displayed") {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
package com.tangem.scenarios
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.extensions.SwipeDirection
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.swipeVertical
|
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.screens.onMarketsExchangesScreen
|
import com.tangem.screens.onMarketsExchangesScreen
|
||||||
import com.tangem.screens.onMarketsScreen
|
import com.tangem.screens.onMarketsScreen
|
||||||
import com.tangem.screens.onMarketsTokenDetailsScreen
|
import com.tangem.screens.onMarketsTokenDetailsScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName: String) {
|
fun BaseTestCase.openTokenDetailsFromMarketsScreen(blockchainName: String, tokenName: String) {
|
||||||
step("Open 'Markets' screen") {
|
step("Open 'Markets' screen") {
|
||||||
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Click on 'Search' placeholder") {
|
|
||||||
onMarketsScreen { searchThroughMarketPlaceholder.performClick() }
|
|
||||||
}
|
|
||||||
step("Click on $blockchainName blockchain") {
|
step("Click on $blockchainName blockchain") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on $tokenName token") {
|
step("Click on 'In your portfolio' block") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsTokenDetailsScreen { inYourPortfolioBlock.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on $tokenName token in 'Your portfolio' bottom sheet") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +59,7 @@ fun BaseTestCase.openMarketsScreen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
||||||
openMarketsScreen()
|
openMarketsScreen()
|
||||||
if (shouldClickSeeAllButton)
|
if (shouldClickSeeAllButton)
|
||||||
|
|
@ -69,9 +70,8 @@ fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAll
|
||||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Scroll down") {
|
step("Scroll to 'Listed on exchanges' block") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
onMarketsScreen { scrollToListedOnBlock() }
|
||||||
swipeVertical(SwipeDirection.UP)
|
|
||||||
}
|
}
|
||||||
step("Click on 'Listed on exchanges' block") {
|
step("Click on 'Listed on exchanges' block") {
|
||||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.assertIsDimmed
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.screens.*
|
import com.tangem.screens.*
|
||||||
|
|
@ -34,8 +33,11 @@ fun BaseTestCase.openSendScreen(
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,11 +93,11 @@ fun BaseTestCase.openSendAddressScreen(
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not dimmed") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
@ -109,6 +111,13 @@ fun BaseTestCase.openSendAddressScreen(
|
||||||
step("Assert 'Send Address' container is displayed") {
|
step("Assert 'Send Address' container is displayed") {
|
||||||
onSendAddressScreen { container.assertIsDisplayed() }
|
onSendAddressScreen { container.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
|
step("Wait for recipient list to load") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching {
|
||||||
|
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||||
|
}.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) {
|
fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) {
|
||||||
|
|
@ -244,8 +253,11 @@ fun BaseTestCase.selectTokenToSendViaSwap(
|
||||||
networkName: String,
|
networkName: String,
|
||||||
networkType: String? = null,
|
networkType: String? = null,
|
||||||
) {
|
) {
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap to another token' button") {
|
step("Click on 'Swap to another token' button") {
|
||||||
onSendScreen { swapToAnotherTokenButton.performClick() }
|
onSendScreen { swapToAnotherTokenButton.performClick() }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.assertVisibility
|
import com.tangem.common.extensions.assertVisibility
|
||||||
|
import com.tangem.common.extensions.clickWhenEnabled
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.isDisplayedSafely
|
import com.tangem.common.extensions.isDisplayedSafely
|
||||||
import com.tangem.core.ui.R as CoreUiR
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
@ -43,7 +44,7 @@ fun BaseTestCase.openSwapScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") {
|
SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.clickWhenEnabled() }
|
||||||
}
|
}
|
||||||
|
|
||||||
SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") {
|
SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class AddFundsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AddFundsBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val buyButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_buy)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val swapButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiveButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_receive)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_close)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAddFundsBottomSheet(function: AddFundsBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -11,7 +11,10 @@ import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
|
||||||
class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<AddTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<AddTokenBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||||
|
|
@ -23,6 +26,12 @@ class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractions
|
||||||
hasText(getResourceString(R.string.common_add))
|
hasText(getResourceString(R.string.common_add))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val laterButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
|
hasText(getResourceString(R.string.common_later))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import androidx.compose.ui.semantics.SemanticsProperties
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
import androidx.compose.ui.test.SemanticsMatcher
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.hasAnyAncestor
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
|
import androidx.compose.ui.test.swipeUp
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.extensions.getQuantityString
|
import com.tangem.common.extensions.getQuantityString
|
||||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||||
|
|
@ -22,7 +24,7 @@ import androidx.compose.ui.test.hasText as withText
|
||||||
import com.tangem.core.res.R as CoreResR
|
import com.tangem.core.res.R as CoreResR
|
||||||
import com.tangem.core.ui.R as CoreUiR
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
private val lazyList = KLazyListNode(
|
private val lazyList = KLazyListNode(
|
||||||
|
|
@ -49,7 +51,8 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
val buyButton: KNode = child {
|
val buyButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_buy))
|
hasAnyDescendant(withText(getResourceString(R.string.common_buy)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val addFundsButton: KNode = child {
|
val addFundsButton: KNode = child {
|
||||||
|
|
@ -59,22 +62,26 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
val sendButton: KNode = child {
|
val sendButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_send))
|
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val receiveButton: KNode = child {
|
val receiveButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_receive))
|
hasAnyDescendant(withText(getResourceString(R.string.common_receive)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val sellButton: KNode = child {
|
val sellButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_sell))
|
hasAnyDescendant(withText(getResourceString(R.string.common_sell)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val swapButton: KNode = child {
|
val swapButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasAnyDescendant(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val walletNameText: KNode = child {
|
val walletNameText: KNode = child {
|
||||||
|
|
@ -87,13 +94,37 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val walletDevicesCount: KNode = child {
|
/**
|
||||||
hasTestTag(MainScreenTestTags.DEVICES_COUNT)
|
* Collapses the collapsing header via a touch-based swipe so that items near the bottom
|
||||||
|
* of the lazy list fall within screen bounds before programmatic childWith scroll.
|
||||||
|
* Required because TangemCollapsingTopBar places the body at y=collapsingHeight, which
|
||||||
|
* pushes lower list items off-screen when the header is expanded.
|
||||||
|
*/
|
||||||
|
private fun collapseHeader() {
|
||||||
|
screenContainer {
|
||||||
|
performTouchInput { swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val restoringProgressText: KNode = child {
|
||||||
|
hasTestTag(MainScreenTestTags.SYNC_PROGRESS_TEXT)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val walletImportedBanner: KNode = child {
|
||||||
|
hasTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val walletImportedBannerCheckHereButton: KNode = child {
|
||||||
|
hasAnyAncestor(withTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER))
|
||||||
|
hasText(getResourceString(CoreResR.string.main_manage_tokens))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun marketPriceBlock(): LazyListItemNode {
|
fun marketPriceBlock(): LazyListItemNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MarketPriceBlockTestTags.BLOCK)
|
hasTestTag(MarketPriceBlockTestTags.BLOCK)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
@ -236,6 +267,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun accountWithName(name: String): LazyListItemNode {
|
fun accountWithName(name: String): LazyListItemNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasAnyDescendant(withText(name))
|
hasAnyDescendant(withText(name))
|
||||||
|
|
@ -243,11 +275,21 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun tokenRowWithTitle(tokenTitle: String): LazyListItemNode {
|
||||||
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
|
hasText(tokenTitle)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find token list item with title and address
|
* Find token list item with title and address
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
|
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -260,6 +302,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -272,6 +315,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun addAndManageButton(): KNode {
|
fun addAndManageButton(): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
||||||
}.child<KNode> {
|
}.child<KNode> {
|
||||||
|
|
@ -287,11 +331,12 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchThroughMarketPlaceholder: KNode = child {
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_search_header_title))
|
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasAnyChild(withText(tokenNetwork))
|
hasAnyChild(withText(tokenNetwork))
|
||||||
|
|
@ -301,6 +346,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
|
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -335,6 +381,12 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertTokensCount(expectedCount: Int) {
|
||||||
|
semanticsProvider
|
||||||
|
.onAllNodes(withTestTag(TokenElementsTestTags.TOKEN_PRICE))
|
||||||
|
.assertCountEquals(expectedCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
|
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.semantics.SemanticsNode
|
import androidx.compose.ui.semantics.SemanticsNode
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import androidx.compose.ui.test.hasParent
|
|
||||||
import androidx.compose.ui.test.hasTestTag
|
import androidx.compose.ui.test.hasTestTag
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
|
||||||
import com.tangem.features.onramp.impl.R
|
import com.tangem.features.onramp.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
|
|
@ -23,16 +21,15 @@ class MarketsExchangesPageObject(private val provider: SemanticsNodeInteractions
|
||||||
|
|
||||||
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
||||||
provider
|
provider
|
||||||
.onAllNodes(hasParent(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))))
|
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))
|
||||||
.fetchSemanticsNodes()
|
.fetchSemanticsNodes()
|
||||||
|
|
||||||
fun allTrustScoreNodes(): List<SemanticsNode> =
|
fun allTrustScoreNodes(): List<SemanticsNode> =
|
||||||
provider
|
provider
|
||||||
.onAllNodes(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)))
|
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT))
|
||||||
.fetchSemanticsNodes()
|
.fetchSemanticsNodes()
|
||||||
|
|
||||||
val exchangesTitle: KNode = child {
|
val exchangesTitle: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.TITLE)
|
|
||||||
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import androidx.compose.ui.test.hasTestTag
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
|
|
@ -15,9 +17,9 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
val addToPortfolioButton: KNode = child {
|
val addButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
hasText(getResourceString(R.string.common_add_to_portfolio))
|
hasText(getResourceString(R.string.common_add))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +33,12 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchThroughMarketPlaceholder: KNode = child {
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_search_header_title))
|
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val tokenDetailsContent: KNode = child {
|
||||||
|
hasTestTag(MarketsTestTags.TOKEN_DETAILS_CONTENT)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +48,8 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val listedOnBlockContainer: KNode = child {
|
val listedOnBlockContainer: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_token_details_listed_on), substring = true)
|
hasTestTag(MarketsTestTags.LISTED_ON_BLOCK)
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val listedOnEmptyText: KNode = child {
|
val listedOnEmptyText: KNode = child {
|
||||||
|
|
@ -60,6 +68,13 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasText(title)
|
hasText(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalTestApi
|
||||||
|
fun scrollToListedOnBlock() {
|
||||||
|
tokenDetailsContent {
|
||||||
|
performScrollToNode(hasTestTag(MarketsTestTags.LISTED_ON_BLOCK))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> Unit) =
|
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package com.tangem.screens
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
||||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
|
||||||
import com.tangem.features.onramp.impl.R
|
import com.tangem.features.onramp.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MarketsTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MarketsTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -20,11 +19,14 @@ class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractions
|
||||||
hasText(getResourceString(R.string.common_swap), substring = true)
|
hasText(getResourceString(R.string.common_swap), substring = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val inYourPortfolioBlock: KNode = child {
|
||||||
|
hasText(getResourceString(CoreUiR.string.markets_portfolio_block_subtitle), substring = true)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun tokenWithTitle(title: String): KNode = child {
|
fun tokenWithTitle(title: String): KNode = child {
|
||||||
hasAnyAncestor(withTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM))
|
|
||||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
|
||||||
hasAnySibling(withTestTag(TokenElementsTestTags.TOKEN_ICON))
|
|
||||||
hasAnyChild(withText(title))
|
hasAnyChild(withText(title))
|
||||||
|
hasClickAction()
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,18 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val topBarGroupButton: KNode = child {
|
val organizeMenuButton: KNode = child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.GROUP_BUTTON)
|
hasTestTag(OrganizeTokensScreenTestTags.MENU_BUTTON)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupButton: KNode = topBarGroupButton.child {
|
val groupButton: KNode = child {
|
||||||
hasText(getResourceString(R.string.organize_tokens_group))
|
hasText(getResourceString(R.string.organize_tokens_group))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val ungroupButton: KNode = topBarGroupButton.child {
|
|
||||||
hasText(getResourceString(R.string.organize_tokens_ungroup))
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortByBalanceButton: KNode = child {
|
val sortByBalanceButton: KNode = child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.SORT_BY_BALANCE_BUTTON)
|
hasText(getResourceString(R.string.organize_tokens_sort_by_balance))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
// endregion TopBar
|
// endregion TopBar
|
||||||
|
|
@ -84,7 +79,7 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM)
|
hasTestTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM)
|
||||||
hasAnyChild(withText(tokenNetwork))
|
hasAnyDescendant(withText(tokenNetwork))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
): KNode = child {
|
): KNode = child {
|
||||||
hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM)
|
hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM)
|
||||||
hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON))
|
hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON))
|
||||||
hasAnyDescendant(withText(recipientAddress))
|
hasAnyDescendant(withText(recipientAddress, substring = true))
|
||||||
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
|
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.utils.LazyListItemNode
|
|
||||||
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.NotificationTestTags
|
import com.tangem.core.ui.test.NotificationTestTags
|
||||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
|
||||||
import com.tangem.features.tokendetails.impl.R
|
import com.tangem.features.tokendetails.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
@ -36,18 +31,8 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val availableStakingBlockTitle: KNode = child {
|
fun availableStakingBlockText(apy: String): KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE)
|
hasText(getResourceString(R.string.token_details_earn_staking_subtitle, apy))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val availableStakingBlockText: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT)
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val availableStakingBlockCurrencyIcon: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON)
|
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,69 +47,39 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingDot: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT)
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val stakingTokenAmount: KNode = child {
|
val stakingTokenAmount: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
|
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingChevronIcon: KNode = child {
|
val stakingTitle: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON)
|
hasText(getResourceString(R.string.common_staking))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingTitle: KNode = child {
|
val stakingEnabledTitle: KNode = child {
|
||||||
hasText(getResourceString(R.string.staking_native))
|
hasText(getResourceString(R.string.staking_enabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val horizontalActionChips = KLazyListNode(
|
val addFundsButton: KNode = child {
|
||||||
semanticsProvider = semanticsProvider,
|
|
||||||
viewBuilderAction = { hasTestTag(BaseActionButtonsBlockTestTags.HORIZONTAL_ACTION_CHIPS) },
|
|
||||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
|
||||||
positionMatcher = { position ->
|
|
||||||
SemanticsMatcher.expectValue(
|
|
||||||
LazyListItemPositionSemantics,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
fun receiveButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_receive))
|
hasAnyDescendant(withText(getResourceString(R.string.tangempay_card_details_add_funds)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
val swapButton: KNode = child {
|
||||||
fun swapButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasAnyDescendant(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
val transferButton: KNode = child {
|
||||||
fun sellButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_sell))
|
hasAnyDescendant(withText(getResourceString(R.string.common_transfer)))
|
||||||
}
|
useUnmergedTree = true
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
fun buyButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
|
||||||
hasText(getResourceString(R.string.common_buy))
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
fun sendButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
|
||||||
hasText(getResourceString(R.string.common_send))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
||||||
|
|
@ -204,7 +159,6 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_CHEVRON_ICON))
|
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class TransferBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<TransferBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val sendButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_send)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val swapButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val sellButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_sell)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_close)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onTransferBottomSheet(function: TransferBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -27,6 +27,7 @@ import com.tangem.screens.onSendScreen
|
||||||
import com.tangem.screens.onStoriesScreen
|
import com.tangem.screens.onStoriesScreen
|
||||||
import com.tangem.screens.onTokenDetailsScreen
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
import com.tangem.screens.onMainScreenTopBar
|
import com.tangem.screens.onMainScreenTopBar
|
||||||
|
import com.tangem.screens.onTransferBottomSheet
|
||||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
|
@ -94,8 +95,11 @@ class FeedbackTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$sendAmount' in input text field") {
|
step("Type '$sendAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
fun groupTokensTest() {
|
fun groupTokensTest() {
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
val tokenTitle = "Ethereum"
|
val tokenTitle = "Ethereum"
|
||||||
val tokenNetwork = "Ethereum network"
|
val networkTitleOrganize = "Ethereum"
|
||||||
|
val networkTitleMain = "Ethereum network"
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
|
|
@ -39,17 +40,20 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
step("Open organize menu") {
|
||||||
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
step("Click 'Group' button") {
|
step("Click 'Group' button") {
|
||||||
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were grouped on 'Organize tokens' screen") {
|
step("Assert tokens were grouped on 'Organize tokens' screen") {
|
||||||
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
|
onOrganizeTokensScreen { tokenNetworkGroupTitle(networkTitleOrganize).assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'Apply' button") {
|
step("Click 'Apply' button") {
|
||||||
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were grouped on 'Main screen'") {
|
step("Assert tokens were grouped on 'Main screen'") {
|
||||||
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
|
onMainScreen { tokenNetworkGroupTitle(networkTitleMain).assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Open 'Organize tokens' screen") {
|
step("Open 'Organize tokens' screen") {
|
||||||
openOrganizeTokensScreen()
|
openOrganizeTokensScreen()
|
||||||
|
|
@ -60,17 +64,20 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Click 'Ungroup' button") {
|
step("Open organize menu") {
|
||||||
onOrganizeTokensScreen { ungroupButton.clickWithAssertion() }
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click 'Group' checkbox again to ungroup") {
|
||||||
|
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were ungrouped on 'Organize tokens' screen") {
|
step("Assert tokens were ungrouped on 'Organize tokens' screen") {
|
||||||
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
|
onOrganizeTokensScreen { tokenNetworkGroupTitle(networkTitleOrganize).assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'Apply' button") {
|
step("Click 'Apply' button") {
|
||||||
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were ungrouped on 'Main screen'") {
|
step("Assert tokens were ungrouped on 'Main screen'") {
|
||||||
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
|
onMainScreen { tokenNetworkGroupTitle(networkTitleMain).assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +192,9 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitleAndPosition(polExMaticTitle, 3).assertIsDisplayed()
|
tokenWithTitleAndPosition(polExMaticTitle, 3).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
step("Open organize menu") {
|
||||||
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
step("Click 'By Balance' button") {
|
step("Click 'By Balance' button") {
|
||||||
onOrganizeTokensScreen {
|
onOrganizeTokensScreen {
|
||||||
sortByBalanceButton.clickWithAssertion()
|
sortByBalanceButton.clickWithAssertion()
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(cardType)
|
openMainScreen(cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '${cardType.name}' $cardBlockchain card") {
|
step("Check 'Main' screen for '${cardType.name}' $cardBlockchain card") {
|
||||||
checkSingleCurrencyMainScreen(
|
checkSingleCurrencyMainScreen(cardTitle = cardType.name)
|
||||||
cardBlockchain = cardBlockchain,
|
|
||||||
cardTitle = cardType.name,
|
|
||||||
withTransactions = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +53,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$cardName' $cardBlockchain card") {
|
step("Check 'Main' screen for '$cardName' $cardBlockchain card") {
|
||||||
checkSingleCurrencyMainScreen(cardBlockchain = cardBlockchain, cardTitle = cardName)
|
checkSingleCurrencyMainScreen(cardTitle = cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +62,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: Card with Secp256k1 curve")
|
@DisplayName("Scan: Card with Secp256k1 curve")
|
||||||
@Test
|
@Test
|
||||||
fun secpk1CurveCardScanTest() {
|
fun secpk1CurveCardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: MockContent = Secpk1CurveMockContent
|
val cardType: MockContent = Secpk1CurveMockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "card with Secp256k1 curve"
|
val card = "card with Secp256k1 curve"
|
||||||
|
|
@ -75,12 +70,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on $card") {
|
step("Open 'Main Screen' on $card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for $card curve") {
|
||||||
checkMultiCurrencyMainScreen(
|
checkMultiCurrencyMainScreen(cardTitle = cardName)
|
||||||
devicesCount = devicesCount,
|
|
||||||
cardTitle = cardName,
|
|
||||||
withWalletImage = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,11 +90,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for $card with blockchain: '$cardBlockchain'") {
|
step("Check 'Main' screen for $card with blockchain: '$cardBlockchain'") {
|
||||||
checkSingleCurrencyMainScreen(
|
checkSingleCurrencyMainScreen(cardTitle = cardName)
|
||||||
cardBlockchain = cardBlockchain,
|
|
||||||
cardTitle = cardName,
|
|
||||||
withWalletImage = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +99,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Shiba' card")
|
@DisplayName("Scan: 'Shiba' card")
|
||||||
@Test
|
@Test
|
||||||
fun shibaCardScanTest() {
|
fun shibaCardScanTest() {
|
||||||
val devicesCount = "2 devices"
|
|
||||||
val cardType: MockContent = ShibaMockContent
|
val cardType: MockContent = ShibaMockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "Shiba"
|
val card = "Shiba"
|
||||||
|
|
@ -121,8 +107,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card' card") {
|
step("Open 'Main Screen' on '$card' card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +117,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Ring'")
|
@DisplayName("Scan: 'Ring'")
|
||||||
@Test
|
@Test
|
||||||
fun ringScanTest() {
|
fun ringScanTest() {
|
||||||
val devicesCount = "3 devices"
|
|
||||||
val cardType: ProductType = ProductType.Ring
|
val cardType: ProductType = ProductType.Ring
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val ring = "Ring"
|
val ring = "Ring"
|
||||||
|
|
@ -140,8 +125,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$ring'") {
|
step("Open 'Main Screen' on '$ring'") {
|
||||||
openMainScreen(productType = cardType)
|
openMainScreen(productType = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$ring'") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +135,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Wallet' card")
|
@DisplayName("Scan: 'Wallet' card")
|
||||||
@Test
|
@Test
|
||||||
fun walletCardScanTest() {
|
fun walletCardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: ProductType = ProductType.Wallet
|
val cardType: ProductType = ProductType.Wallet
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
|
|
||||||
|
|
@ -158,8 +142,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$cardName' card") {
|
step("Open 'Main Screen' on '$cardName' card") {
|
||||||
openMainScreen(productType = cardType)
|
openMainScreen(productType = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$cardName' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$cardName' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +152,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Wallet 2' card")
|
@DisplayName("Scan: 'Wallet 2' card")
|
||||||
@Test
|
@Test
|
||||||
fun wallet2ScanTest() {
|
fun wallet2ScanTest() {
|
||||||
val devicesCount = "2 devices"
|
|
||||||
val cardType: MockContent = Wallet2MockContent
|
val cardType: MockContent = Wallet2MockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "Wallet 2"
|
val card = "Wallet 2"
|
||||||
|
|
@ -177,8 +160,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card' card") {
|
step("Open 'Main Screen' on '$card' card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +170,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: Card with 4.12 firmware")
|
@DisplayName("Scan: Card with 4.12 firmware")
|
||||||
@Test
|
@Test
|
||||||
fun firmware412CardScanTest() {
|
fun firmware412CardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: MockContent = Firmware412MockContent
|
val cardType: MockContent = Firmware412MockContent
|
||||||
val cardName = "Tangem card"
|
val cardName = "Tangem card"
|
||||||
val card = "card with 4.12 firmware"
|
val card = "card with 4.12 firmware"
|
||||||
|
|
@ -196,8 +178,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card'") {
|
step("Open 'Main Screen' on '$card'") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card'") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,20 +56,14 @@ class StakingTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking title' is displayed") {
|
step("Assert 'Staking title' is displayed") {
|
||||||
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingEnabledTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking fiat amount' is displayed") {
|
step("Assert 'Staking fiat amount' is displayed") {
|
||||||
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking dot' is displayed") {
|
|
||||||
onTokenDetailsScreen { stakingDot.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Staking token amount' is displayed") {
|
step("Assert 'Staking token amount' is displayed") {
|
||||||
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking block chevron icon' is displayed") {
|
|
||||||
onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +133,7 @@ class StakingTest : BaseTestCase() {
|
||||||
val scenarioName = "staking_eth_pol_balances_android"
|
val scenarioName = "staking_eth_pol_balances_android"
|
||||||
val scenarioState = "Started"
|
val scenarioState = "Started"
|
||||||
val stakingAmount = "1"
|
val stakingAmount = "1"
|
||||||
|
val stakingApy = "2.84%"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
|
|
@ -172,13 +167,10 @@ class StakingTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
|
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Available staking block' title is displayed") {
|
step("Assert 'Available staking block' title is displayed") {
|
||||||
onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Available staking block' text is displayed") {
|
step("Assert 'Available staking block' text is displayed") {
|
||||||
onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() }
|
onTokenDetailsScreen { availableStakingBlockText(stakingApy).assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Assert 'Available staking block' currency icon is displayed") {
|
|
||||||
onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Click on 'Stake' button") {
|
step("Click on 'Stake' button") {
|
||||||
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
|
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
package com.tangem.tests
|
package com.tangem.tests
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
|
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
|
||||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithSeedPhraseMockContent
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,9 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
step("Click on 'Buy' button") {
|
step("Click on 'Buy' button") {
|
||||||
onMainScreen { buyButton.performClick() }
|
onMainScreen { buyButton.performClick() }
|
||||||
}
|
}
|
||||||
|
step("Click on token: '$tokenTitle'") {
|
||||||
|
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).performClick() }
|
||||||
|
}
|
||||||
step("Click on 'Confirm' button in 'Dialog'") {
|
step("Click on 'Confirm' button in 'Dialog'") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onDialog { confirmButton.clickWithAssertion() }
|
onDialog { confirmButton.clickWithAssertion() }
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,17 @@ package com.tangem.tests.actionButtons
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.extensions.assertIsDimmed
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
||||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onAddFundsBottomSheet
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.screens.onSwapStoriesScreen
|
import com.tangem.screens.onSwapStoriesScreen
|
||||||
import com.tangem.screens.onSwapTokenScreen
|
import com.tangem.screens.onSwapTokenScreen
|
||||||
import com.tangem.screens.onTokenDetailsScreen
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
|
import com.tangem.screens.onTransferBottomSheet
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
|
@ -37,20 +38,41 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is displayed") {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
onTokenDetailsScreen { receiveButton().assertIsDisplayed() }
|
onTokenDetailsScreen { addFundsButton.assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Assert 'Buy' button is displayed") {
|
|
||||||
onTokenDetailsScreen { buyButton().assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Send' button is displayed") {
|
|
||||||
onTokenDetailsScreen { sendButton().assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is displayed") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onTokenDetailsScreen { sellButton().assertIsDisplayed() }
|
onTokenDetailsScreen { transferButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Add funds' button") {
|
||||||
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Buy' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { buyButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { swapButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Receive' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { swapButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Sell' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { sellButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,20 +94,41 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is not dimmed") {
|
step("Assert 'Add funds' button is enabled") {
|
||||||
onTokenDetailsScreen { receiveButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { addFundsButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Buy' button is not dimmed") {
|
step("Assert 'Swap' button is disabled") {
|
||||||
onTokenDetailsScreen { buyButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not dimmed") {
|
step("Assert 'Transfer' button is enabled") {
|
||||||
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { transferButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Click on 'Add funds' button") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is dimmed") {
|
step("Assert 'Buy' button in bottom sheet is enabled") {
|
||||||
onTokenDetailsScreen { sellButton().assertIsDimmed() }
|
onAddFundsBottomSheet { buyButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||||
|
onAddFundsBottomSheet { swapButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Receive' button in bottom sheet is enabled") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button in bottom sheet is enabled") {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||||
|
onTransferBottomSheet { swapButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Sell' button in bottom sheet is disabled") {
|
||||||
|
onTransferBottomSheet { sellButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +152,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -140,8 +183,11 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Receive' button") {
|
step("Click on 'Add funds' button") {
|
||||||
onTokenDetailsScreen { receiveButton().performClick() }
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Go to QR code bottom sheet") {
|
step("Go to QR code bottom sheet") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package com.tangem.tests.balance
|
|
||||||
|
|
||||||
import androidx.compose.ui.test.longClick
|
|
||||||
import com.tangem.common.BaseTestCase
|
|
||||||
import com.tangem.scenarios.openMainScreen
|
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
|
||||||
import com.tangem.screens.onMainScreen
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
|
||||||
import io.qameta.allure.kotlin.AllureId
|
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
@HiltAndroidTest
|
|
||||||
class TotalBalanceLongTapTest : BaseTestCase() {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@AllureId("3965")
|
|
||||||
@DisplayName("Total balance: check long tap on block without biometry")
|
|
||||||
fun whenBiometryIsOffTest() {
|
|
||||||
setupHooks().run {
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Long tap on total balance block") {
|
|
||||||
onMainScreen {
|
|
||||||
totalBalanceContainer.performTouchInput {
|
|
||||||
longClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert 'Rename' button is displayed") {
|
|
||||||
onMainScreen { totalBalanceMenuRenameWallet.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Delete' button is not displayed") {
|
|
||||||
onMainScreen { totalBalanceMenuDeleteWallet.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.balance
|
||||||
import androidx.compose.ui.test.longClick
|
import androidx.compose.ui.test.longClick
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
|
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.extensions.*
|
import com.tangem.common.extensions.*
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -82,28 +83,27 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
}
|
}
|
||||||
step("Click on 'Add to portfolio' button") {
|
step("Click on 'Add' button in 'Markets' bottom sheet") {
|
||||||
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
|
onMarketsScreen { addButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on main network") {
|
step("Click on 'Add' button in 'Add token' bottom sheet") {
|
||||||
onMarketsScreen { mainNetworkSuffix.performClick() }
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
onAddTokenBottomSheet {
|
||||||
|
addButton.performClick()
|
||||||
}
|
}
|
||||||
step("Click on 'Add' button") {
|
onAddTokenBottomSheet { laterButton.assertIsDisplayed() }
|
||||||
onDialog { addButton.clickWithAssertion() }
|
|
||||||
}
|
}
|
||||||
step("Assert 'Continue' is not displayed") {
|
|
||||||
onDialog { addButton.assertIsNotDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Click on 'Later' button") {
|
step("Click on 'Later' button") {
|
||||||
onDialog { laterButton.clickWithAssertion() }
|
onAddTokenBottomSheet { laterButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Go back to 'Markets: tokens list'") {
|
step("Press 'Back' button") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsScreen { topBarBackButton.clickWithAssertion() }
|
device.uiDevice.pressBack()
|
||||||
}
|
}
|
||||||
step("Close 'Markets screen'") {
|
step("Press 'Back' button") {
|
||||||
onSearchBar { searchField.assertIsDisplayed() }
|
waitForIdle()
|
||||||
swipeMarketsBlock(SwipeDirection.DOWN)
|
device.uiDevice.pressBack()
|
||||||
}
|
}
|
||||||
step("Assert $updatedBalance is displayed in total balance") {
|
step("Assert $updatedBalance is displayed in total balance") {
|
||||||
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
|
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
package com.tangem.tests.hotWallet
|
||||||
|
|
||||||
|
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.CREATE_USER_WALLET_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.PROVIDERS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.SEED_PHRASE_HAPPY_PATH
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WALLET_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.restartApp
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class AssetsDiscoveryTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DISCOVERY_TIMEOUT_MILLIS = 120_000L
|
||||||
|
|
||||||
|
const val SCENARIO_STATE_STARTED = "Started"
|
||||||
|
const val SCENARIO_STATE_EMPTY = "Empty"
|
||||||
|
const val SCENARIO_STATE_ALREADY_EXISTS = "AlreadyExists"
|
||||||
|
const val SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT = "AssetsDiscoveryRedirect"
|
||||||
|
const val SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH = "AssetsDiscoveryHappyPath"
|
||||||
|
const val SCENARIO_STATE_NON_ZERO_EVM_BALANCES = "NonZeroEvmBalances"
|
||||||
|
const val SCENARIO_STATE_NON_ZERO_EVM_BALANCES_SLOW = "NonZeroEvmBalancesSlow"
|
||||||
|
|
||||||
|
val EXPECTED_DISCOVERED_TOKENS = listOf(
|
||||||
|
"Ethereum",
|
||||||
|
"Polygon",
|
||||||
|
"Tether",
|
||||||
|
)
|
||||||
|
|
||||||
|
val TOKENS_THAT_MUST_NOT_APPEAR = listOf(
|
||||||
|
"Solana",
|
||||||
|
"USDC",
|
||||||
|
)
|
||||||
|
|
||||||
|
val BACKEND_PRE_POPULATED_TOKENS = listOf(
|
||||||
|
"Bitcoin",
|
||||||
|
"Ethereum",
|
||||||
|
"Polygon",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9280")
|
||||||
|
@DisplayName("Hot wallet: new import — Discovery → Sync → Banner → Check here happy path")
|
||||||
|
@Test
|
||||||
|
fun newHotWalletImportHappyPathTest() {
|
||||||
|
val packageName = getTargetContext().packageName
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(PROVIDERS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT)
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH)
|
||||||
|
setWireMockScenarioState(WALLET_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO, state = SCENARIO_STATE_NON_ZERO_EVM_BALANCES)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(WALLET_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import a new hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_HAPPY_PATH)
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' progress loader is shown (discovery is in flight)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Wait for 'Wallet successfully imported' banner (discovery completes)") {
|
||||||
|
flakySafely(timeoutMs = DISCOVERY_TIMEOUT_MILLIS) {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert expected discovered tokens are visible in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
EXPECTED_DISCOVERED_TOKENS.forEach { token ->
|
||||||
|
tokenRowWithTitle(token).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Tap 'Check here' (Manage tokens) on the banner") {
|
||||||
|
onMainScreen { walletImportedBannerCheckHereButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Manage Tokens' screen is opened") {
|
||||||
|
onManageTokensScreen { searchField.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Return to main screen") {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Assert banner is hidden after navigating into Manage Tokens") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Force-close and re-launch the app") {
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
step("Assert banner is NOT shown again after relaunch") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert previously discovered tokens still appear in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
EXPECTED_DISCOVERED_TOKENS.forEach { token ->
|
||||||
|
tokenRowWithTitle(token).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert zero-balance and spam tokens are NOT shown in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
TOKENS_THAT_MUST_NOT_APPEAR.forEach { token ->
|
||||||
|
assertTokenDoesNotExist(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9284")
|
||||||
|
@DisplayName("Hot wallet: token added manually during Discovery — no duplicate created")
|
||||||
|
@Test
|
||||||
|
fun manualTokenAddDuringDiscoveryNoDuplicateTest() {
|
||||||
|
val tetherTitle = "Tether"
|
||||||
|
val ethereumNetworkTitle = "ETHEREUM"
|
||||||
|
val accountName = getResourceString(CoreUiR.string.account_main_account_title)
|
||||||
|
val expectedTokensCount = 4
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(PROVIDERS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT)
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH)
|
||||||
|
setWireMockScenarioState(WALLET_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(
|
||||||
|
MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO,
|
||||||
|
state = SCENARIO_STATE_NON_ZERO_EVM_BALANCES_SLOW,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(WALLET_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import a new hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_HAPPY_PATH)
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' progress loader is shown (discovery is in flight)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open wallet details from top bar") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'Wallet settings'") {
|
||||||
|
onDetailsScreen { walletNameButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open account: '$accountName'") {
|
||||||
|
onWalletSettingsScreen { accountItem(accountName).performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Manage Tokens' from account details") {
|
||||||
|
onAccountDetailsScreen { manageTokensButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Search for '$tetherTitle' in Manage Tokens") {
|
||||||
|
onManageTokensScreen {
|
||||||
|
searchField.performClick()
|
||||||
|
searchField.performTextInput(tetherTitle)
|
||||||
|
}
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Expand '$tetherTitle'") {
|
||||||
|
onManageTokensScreen { tokenItem(tetherTitle).clickWithAssertion() }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Enable the $ethereumNetworkTitle network") {
|
||||||
|
onManageTokensScreen { networkSwitch(ethereumNetworkTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Save Manage Tokens changes") {
|
||||||
|
onManageTokensScreen { saveButton.clickWithAssertion() }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Navigate back to main screen") {
|
||||||
|
repeat(times = 3) {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Wait for 'Wallet successfully imported' banner (discovery completes after delay)") {
|
||||||
|
flakySafely(timeoutMs = DISCOVERY_TIMEOUT_MILLIS) {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$tetherTitle' is in the assets list (manual add + discovery merged)") {
|
||||||
|
onMainScreen { tokenRowWithTitle(tetherTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert assets list contains exactly $expectedTokensCount tokens (no duplicate after merge)") {
|
||||||
|
onMainScreen { assertTokensCount(expectedTokensCount) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9282")
|
||||||
|
@DisplayName("Hot wallet: re-import existing wallet — 200 OK, no Discovery, tokens from backend")
|
||||||
|
@Test
|
||||||
|
fun reimportExistingHotWalletTest() {
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_ALREADY_EXISTS)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO, state = SCENARIO_STATE_EMPTY)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import an existing hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_12)
|
||||||
|
}
|
||||||
|
step("Assert tokens from backend are displayed immediately") {
|
||||||
|
BACKEND_PRE_POPULATED_TOKENS.forEach { token ->
|
||||||
|
onMainScreen { tokenRowWithTitle(token).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' loader is NOT displayed (discovery did not start)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Wallet successfully imported' banner is NOT displayed") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package com.tangem.tests.main
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.SwipeDirection
|
||||||
|
import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -37,7 +39,7 @@ class MainScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8748")
|
@AllureId("8748")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with single token no accounts")
|
@DisplayName("Main: check 'Add & Manage' button with single token no accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() {
|
fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() {
|
||||||
val scenarioState = "Cardano"
|
val scenarioState = "Cardano"
|
||||||
|
|
@ -58,14 +60,14 @@ class MainScreenTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Assert 'Add & Manage' button is not displayed") {
|
step("Assert 'Add & Manage' button is displayed") {
|
||||||
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
|
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8749")
|
@AllureId("8749")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with single token two accounts")
|
@DisplayName("Main: check 'Add & Manage' button with single token two accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() {
|
fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() {
|
||||||
val scenarioState = "TwoAccountsSingleTokenEach"
|
val scenarioState = "TwoAccountsSingleTokenEach"
|
||||||
|
|
@ -99,7 +101,7 @@ class MainScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8750")
|
@AllureId("8750")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with multiple tokens two accounts")
|
@DisplayName("Main: check 'Add & Manage' button with multiple tokens two accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() {
|
fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() {
|
||||||
val scenarioState = "TwoAccountsMixed"
|
val scenarioState = "TwoAccountsMixed"
|
||||||
|
|
@ -117,8 +119,11 @@ class MainScreenTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
}
|
}
|
||||||
|
step("Swipe up") {
|
||||||
|
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f, endHeightRatio = 0.1f)
|
||||||
|
}
|
||||||
step("Assert 'Add & Manage' button is displayed") {
|
step("Assert 'Add & Manage' button is displayed") {
|
||||||
onMainScreen { addAndManageButtonNode.assertIsDisplayed()}
|
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import org.junit.Test
|
||||||
class WarningsTest : BaseTestCase() {
|
class WarningsTest : BaseTestCase() {
|
||||||
|
|
||||||
@AllureId("184")
|
@AllureId("184")
|
||||||
@DisplayName("Token list: hide token by long tap")
|
@DisplayName("Warnings: missing address warning")
|
||||||
@Test
|
@Test
|
||||||
fun checkUnavailableNetworksWarningTest() {
|
fun checkUnavailableNetworksWarningTest() {
|
||||||
val scenarioState = "MissingDerivation"
|
val scenarioState = "MissingDerivation"
|
||||||
|
|
@ -38,9 +38,6 @@ class WarningsTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses(isBalanceAvailable = false)
|
synchronizeAddresses(isBalanceAvailable = false)
|
||||||
}
|
}
|
||||||
step("Assert 'Missing addresses' notification icon is displayed") {
|
|
||||||
onMainScreen { missingAddressNotificationIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Missing addresses' notification title is displayed") {
|
step("Assert 'Missing addresses' notification title is displayed") {
|
||||||
onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() }
|
onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.tests.markets
|
package com.tangem.tests.markets
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.annotations.ApiEnv
|
import com.tangem.common.annotations.ApiEnv
|
||||||
import com.tangem.common.annotations.ApiEnvConfig
|
import com.tangem.common.annotations.ApiEnvConfig
|
||||||
|
|
@ -39,6 +40,7 @@ class MarketsExchangesTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
@Test
|
@Test
|
||||||
@AllureId("56")
|
@AllureId("56")
|
||||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||||
|
|
@ -60,9 +62,8 @@ class MarketsExchangesTest : BaseTestCase() {
|
||||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Scroll down") {
|
step("Scroll to 'Listed on exchanges' block") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
onMarketsScreen { scrollToListedOnBlock() }
|
||||||
swipeVertical(SwipeDirection.UP)
|
|
||||||
}
|
}
|
||||||
step("Assert 'Listed on exchanges' block has title") {
|
step("Assert 'Listed on exchanges' block has title") {
|
||||||
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ class RecentBlockTest : BaseTestCase() {
|
||||||
val sendAmount = "1"
|
val sendAmount = "1"
|
||||||
val txHistoryScenarioState = "11OutgoingTransactions"
|
val txHistoryScenarioState = "11OutgoingTransactions"
|
||||||
val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq"
|
val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq"
|
||||||
val shortenedRecipientAddress = "DJ2TaZ5vvp3mBLugU...Li4uYaq123456789b"
|
val longRecipientAddress = recipientAddressBase + "123456789b"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
|
|
@ -261,7 +261,7 @@ class RecentBlockTest : BaseTestCase() {
|
||||||
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = recentTransactionAmount1)
|
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = recentTransactionAmount1)
|
||||||
}
|
}
|
||||||
step("Check recent address item №2") {
|
step("Check recent address item №2") {
|
||||||
checkRecentAddressItem(address = shortenedRecipientAddress, description = recentTransactionAmount2)
|
checkRecentAddressItem(address = longRecipientAddress, description = recentTransactionAmount2)
|
||||||
}
|
}
|
||||||
step("Check recent address item №3") {
|
step("Check recent address item №3") {
|
||||||
checkRecentAddressItem(address = recipientAddressBase + "k", description = recentTransactionAmount2)
|
checkRecentAddressItem(address = recipientAddressBase + "k", description = recentTransactionAmount2)
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,11 @@ class SendAddressScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,11 @@ class SendConfirmScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
@ -123,8 +126,11 @@ class SendConfirmScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
|
||||||
|
|
@ -281,13 +281,13 @@ class SendFeeScreenTest : BaseTestCase() {
|
||||||
fun checkNetworkFeeBottomSheetForBitcoinTest() {
|
fun checkNetworkFeeBottomSheetForBitcoinTest() {
|
||||||
val tokenName = "Bitcoin"
|
val tokenName = "Bitcoin"
|
||||||
val tokenAmount = "0.00000001"
|
val tokenAmount = "0.00000001"
|
||||||
val feeAmount = "$2.86"
|
val feeAmount = "$0.48"
|
||||||
val fiatFeeAmount = "$0.24"
|
val fiatFeeAmount = "$0.24"
|
||||||
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
val feeUpTo = getResourceString(R.string.send_max_fee)
|
val feeUpTo = getResourceString(R.string.send_max_fee)
|
||||||
val feeUpToValue = "0.0000264 BTC"
|
val feeUpToValue = "0.0000044 BTC"
|
||||||
val newFeeUpToValue = "0.0000022 BTC"
|
val newFeeUpToValue = "0.0000022 BTC"
|
||||||
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
|
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
|
||||||
val satoshiValue = "2"
|
val satoshiValue = "2"
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.KASPA_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.KASPA_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.checkSendWarning
|
import com.tangem.scenarios.checkSendWarning
|
||||||
|
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||||
import com.tangem.scenarios.openSendScreen
|
import com.tangem.scenarios.openSendScreen
|
||||||
import com.tangem.screens.onSendAddressScreen
|
import com.tangem.screens.onSendAddressScreen
|
||||||
import com.tangem.screens.onSendScreen
|
import com.tangem.screens.onSendScreen
|
||||||
|
|
@ -145,8 +147,10 @@ class KaspaWarningsTest : BaseTestCase() {
|
||||||
step("Type address in input text field") {
|
step("Type address in input text field") {
|
||||||
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
}
|
}
|
||||||
step("Click on 'Next' button") {
|
step("Click 'Next' button until 'Send Confirm' screen opens") {
|
||||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { openSendConfirmScreenViaNextButton() }.isSuccess
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'UTXO limit warning' is displayed") {
|
step("Assert 'UTXO limit warning' is displayed") {
|
||||||
checkSendWarning(
|
checkSendWarning(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ package com.tangem.tests.swap
|
||||||
import androidx.compose.ui.test.longClick
|
import androidx.compose.ui.test.longClick
|
||||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
|
||||||
import com.tangem.common.extensions.assertHasBadge
|
|
||||||
import com.tangem.common.extensions.restartApp
|
import com.tangem.common.extensions.restartApp
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -18,101 +16,6 @@ import org.junit.Test
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class SwapStoriesTest : BaseTestCase() {
|
class SwapStoriesTest : BaseTestCase() {
|
||||||
|
|
||||||
@AllureId("5453")
|
|
||||||
@DisplayName("Check 'Swap' button badge on 'Main' screen")
|
|
||||||
@Test
|
|
||||||
fun checkMainScreenSwapButtonBadgeTest() {
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5454")
|
|
||||||
@DisplayName("Check 'Swap' button badge on token details screen")
|
|
||||||
@Test
|
|
||||||
fun checkTokenDetailsScreenSwapButtonTest() {
|
|
||||||
val tokenName = "Ethereum"
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Click on token with name: '$tokenName'") {
|
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5455")
|
|
||||||
@DisplayName("Check 'Swap' button badge on token details in 'Market' screen")
|
|
||||||
@Test
|
|
||||||
fun checkMarketTokenDetailsScreenSwapButtonTest() {
|
|
||||||
val tokenName = "Ethereum"
|
|
||||||
val badgeShown = "Badge shown"
|
|
||||||
val badgeHidden = "Badge hidden"
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5469")
|
@AllureId("5469")
|
||||||
@DisplayName("Check unavailable swap stories on 'Main' screen")
|
@DisplayName("Check unavailable swap stories on 'Main' screen")
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -136,9 +39,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
@ -155,9 +55,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { swapButton.assertIsDisplayed() }
|
onMainScreen { swapButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true)
|
||||||
}
|
}
|
||||||
|
|
@ -192,9 +89,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
@ -207,13 +101,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Restart app") {
|
step("Restart app") {
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
waitForIdle()
|
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
|
||||||
composeTestRule.mainClock.advanceTimeBy(500)
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||||
}
|
}
|
||||||
|
|
@ -228,8 +115,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
val scenarioErrorState = "Error"
|
val scenarioErrorState = "Error"
|
||||||
val packageName = getTargetContext().packageName
|
val packageName = getTargetContext().packageName
|
||||||
val tokenName = "Ethereum"
|
val tokenName = "Ethereum"
|
||||||
val badgeShown = "Badge shown"
|
|
||||||
val badgeHidden = "Badge hidden"
|
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeAppLaunchSection = {
|
additionalBeforeAppLaunchSection = {
|
||||||
|
|
@ -246,16 +131,15 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Open 'Token details' from 'Markets' screen for token '$tokenName'") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
openTokenDetailsFromMarketsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
step("Click on 'Close' button") {
|
step("Click on 'Close' button") {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
|
@ -266,16 +150,12 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Restart app") {
|
step("Restart app") {
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,11 +211,8 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Click on 'Swap' button on 'Token details' screen") {
|
step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check stories changes") {
|
step("Check stories changes") {
|
||||||
checkStoriesChanges()
|
checkStoriesChanges()
|
||||||
|
|
@ -369,11 +246,11 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Open 'Token details' from 'Markets' screen for token '$tokenName'") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
openTokenDetailsFromMarketsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button on 'Markets' token details screen") {
|
step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check stories changes") {
|
step("Check stories changes") {
|
||||||
checkStoriesChanges()
|
checkStoriesChanges()
|
||||||
|
|
@ -388,7 +265,7 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen without stories") {
|
step("Open 'Swap' screen without stories") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -433,6 +310,17 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on 'Close' button") {
|
step("Click on 'Close' button") {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
}
|
}
|
||||||
|
step("Long click on token with name: '$tokenName' again to reopen actions menu") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen {
|
||||||
|
tokenWithTitleAndAddress(tokenName).performTouchInput {
|
||||||
|
longClick(
|
||||||
|
position = center,
|
||||||
|
durationMillis = 1000L,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
step("Open 'Swap' screen without stories") {
|
step("Open 'Swap' screen without stories") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -147,7 +147,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
disableMobileData()
|
disableMobileData()
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -201,7 +201,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -304,7 +304,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -510,7 +510,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is not dimmed. Swap available") {
|
step("Assert 'Swap' button is not dimmed. Swap available") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { swapButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -519,7 +519,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -528,7 +528,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,17 @@
|
||||||
android:scheme="tangem" />
|
android:scheme="tangem" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="survey"
|
||||||
|
android:scheme="tangem" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.abtests.manager.ABTestsManager
|
||||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||||
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
|
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
|
||||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||||
|
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
|
|
@ -49,4 +50,6 @@ interface ApplicationEntryPoint {
|
||||||
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
||||||
|
|
||||||
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
|
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
|
||||||
|
|
||||||
|
fun getDeviceKeyManager(): DeviceKeyManager
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
import com.tangem.domain.common.LogConfig
|
import com.tangem.domain.common.LogConfig
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||||
|
|
@ -92,6 +93,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val sendTransactionSignerInfoInterceptor
|
private val sendTransactionSignerInfoInterceptor
|
||||||
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
|
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
|
||||||
|
|
||||||
|
private val deviceKeyManager: DeviceKeyManager
|
||||||
|
get() = entryPoint.getDeviceKeyManager()
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
private val appScope = MainScope()
|
private val appScope = MainScope()
|
||||||
|
|
@ -132,6 +136,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
|
appScope.launch {
|
||||||
|
deviceKeyManager.generateIfMissing()
|
||||||
|
}
|
||||||
walletsRepository = entryPoint.getWalletsRepository()
|
walletsRepository = entryPoint.getWalletsRepository()
|
||||||
|
|
||||||
apiConfigsManager.initialize()
|
apiConfigsManager.initialize()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.analytics.models.event.SignIn
|
import com.tangem.core.analytics.models.event.SignIn
|
||||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||||
|
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||||
|
|
||||||
class HotWalletContextInterceptor(
|
class HotWalletContextInterceptor(
|
||||||
val parent: ParamsInterceptor? = null,
|
val parent: ParamsInterceptor? = null,
|
||||||
|
|
@ -18,6 +19,7 @@ class HotWalletContextInterceptor(
|
||||||
is SignIn.ButtonAddWallet,
|
is SignIn.ButtonAddWallet,
|
||||||
is SignIn.ButtonUnlockAllWithBiometric,
|
is SignIn.ButtonUnlockAllWithBiometric,
|
||||||
is IntroductionProcess.ButtonScanCard,
|
is IntroductionProcess.ButtonScanCard,
|
||||||
|
is TokenScreenAnalyticsEvent.ButtonQuickTopUp,
|
||||||
-> false
|
-> false
|
||||||
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
|
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
|
||||||
else -> true
|
else -> true
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.tangem.common.routing.DeepLinkScheme
|
import com.tangem.common.routing.DeepLinkScheme
|
||||||
import com.tangem.common.uri.ExternalUrlValidator
|
import com.tangem.common.uri.ExternalUrlValidator
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||||
|
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||||
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
||||||
import com.tangem.core.navigation.url.UrlOpener
|
import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
|
@ -17,6 +19,7 @@ import com.tangem.utils.logging.TangemLogger
|
||||||
internal class DefaultDeeplinkLauncher(
|
internal class DefaultDeeplinkLauncher(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
|
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
) : DeeplinkLauncher {
|
) : DeeplinkLauncher {
|
||||||
|
|
||||||
override fun launch(link: String) {
|
override fun launch(link: String) {
|
||||||
|
|
@ -58,7 +61,26 @@ internal class DefaultDeeplinkLauncher(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchDeepLink(uri: Uri) {
|
private fun launchDeepLink(uri: Uri) {
|
||||||
context.startActivity(createDeepLinkIntent(uri))
|
val intent = createDeepLinkIntent(uri)
|
||||||
|
if (intent.resolveActivity(context.packageManager) != null) {
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
TangemLogger.i(
|
||||||
|
"""
|
||||||
|
No match found for deep link
|
||||||
|
|- Received URI: $uri
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
analyticsExceptionHandler.sendException(
|
||||||
|
ExceptionAnalyticsEvent(
|
||||||
|
exception = UnresolvedDeeplinkException(uri),
|
||||||
|
params = mapOf(
|
||||||
|
"uri_scheme" to uri.scheme.orEmpty(),
|
||||||
|
"uri_host" to uri.host.orEmpty(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||||
|
|
@ -66,3 +88,6 @@ internal class DefaultDeeplinkLauncher(
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class UnresolvedDeeplinkException(uri: Uri) :
|
||||||
|
RuntimeException("Deeplink has no matching activity: scheme=${uri.scheme}, host=${uri.host}")
|
||||||
|
|
@ -4,15 +4,20 @@ import android.app.Application
|
||||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||||
import com.tangem.Log
|
import com.tangem.Log
|
||||||
import com.tangem.TangemSdkLogger
|
import com.tangem.TangemSdkLogger
|
||||||
|
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||||
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
|
import com.tangem.datasource.local.logs.SensitiveUrlMasker
|
||||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
import com.tangem.domain.common.LogConfig
|
import com.tangem.domain.common.LogConfig
|
||||||
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
||||||
|
import com.tangem.utils.JsonStringValuesExtractor
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import com.tangem.wallet.BuildConfig
|
import com.tangem.wallet.BuildConfig
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owns all app-startup wiring of the logging subsystem in a single place:
|
* Owns all app-startup wiring of the logging subsystem in a single place:
|
||||||
|
|
@ -23,12 +28,15 @@ import com.tangem.wallet.BuildConfig
|
||||||
* @property appLogsStore app logs store used by file-based writer and the network logs save
|
* @property appLogsStore app logs store used by file-based writer and the network logs save
|
||||||
* interceptor
|
* interceptor
|
||||||
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
|
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
|
||||||
|
* @property environmentConfig source of [BlockchainSdkConfig] used to build the blockchain
|
||||||
|
* URL masker
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
class TangemLoggingInitializer(
|
class TangemLoggingInitializer(
|
||||||
private val appLogsStore: AppLogsStore,
|
private val appLogsStore: AppLogsStore,
|
||||||
private val tangemSdkLogger: TangemSdkLogger,
|
private val tangemSdkLogger: TangemSdkLogger,
|
||||||
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun initAppLogging() {
|
fun initAppLogging() {
|
||||||
|
|
@ -64,6 +72,13 @@ class TangemLoggingInitializer(
|
||||||
}
|
}
|
||||||
add(createNetworkLoggingInterceptor())
|
add(createNetworkLoggingInterceptor())
|
||||||
add(ChuckerInterceptor(application))
|
add(ChuckerInterceptor(application))
|
||||||
|
add(
|
||||||
|
NetworkLogsSaveInterceptor(
|
||||||
|
appLogsStore = appLogsStore,
|
||||||
|
sensitiveUrlMasker = createBlockchainSensitiveUrlMasker(),
|
||||||
|
shouldCheckResponseBodySize = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TangemApiServiceSettings.addInterceptors(
|
TangemApiServiceSettings.addInterceptors(
|
||||||
|
|
@ -77,4 +92,16 @@ class TangemLoggingInitializer(
|
||||||
}.toTypedArray(),
|
}.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createBlockchainSensitiveUrlMasker(): SensitiveUrlMasker {
|
||||||
|
val json = Json.encodeToJsonElement(
|
||||||
|
BlockchainSdkConfig.serializer(),
|
||||||
|
environmentConfig.blockchainSdkConfig,
|
||||||
|
)
|
||||||
|
// Drop URL-shaped values (e.g. public endpoint URLs from BlockchainSdkConfig like
|
||||||
|
// kaspaSecondaryApiUrl); they are not secrets and would obscure unrelated requests in logs.
|
||||||
|
val values = JsonStringValuesExtractor.extract(json)
|
||||||
|
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }
|
||||||
|
return SensitiveUrlMasker(values)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,12 +5,10 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||||
import com.tangem.domain.card.BuildConfig
|
import com.tangem.domain.card.BuildConfig
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.sdk.api.TangemSdkManager
|
import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||||
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
||||||
import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
|
|
||||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||||
|
|
@ -34,8 +32,6 @@ internal class TangemSdkManagerModule {
|
||||||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||||
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
|
||||||
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
|
||||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||||
cardRepository: CardRepository,
|
cardRepository: CardRepository,
|
||||||
): TangemSdkManager {
|
): TangemSdkManager {
|
||||||
|
|
@ -49,8 +45,6 @@ internal class TangemSdkManagerModule {
|
||||||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||||
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
|
|
||||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
|
||||||
analyticsErrorHandler = analyticsErrorHandler,
|
analyticsErrorHandler = analyticsErrorHandler,
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di
|
package com.tangem.tap.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||||
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
|
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
|
||||||
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
||||||
import com.tangem.core.navigation.finisher.AppFinisher
|
import com.tangem.core.navigation.finisher.AppFinisher
|
||||||
|
|
@ -55,7 +56,10 @@ internal interface UtilsModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
|
fun provideDeeplinkLauncher(
|
||||||
DefaultDeeplinkLauncher(context, urlOpener)
|
@ApplicationContext context: Context,
|
||||||
|
urlOpener: UrlOpener,
|
||||||
|
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
|
): DeeplinkLauncher = DefaultDeeplinkLauncher(context, urlOpener, analyticsExceptionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di.data
|
package com.tangem.tap.di.data
|
||||||
|
|
||||||
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
|
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
|
||||||
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.tap.common.log.TangemBlockchainSDKLogger
|
import com.tangem.tap.common.log.TangemBlockchainSDKLogger
|
||||||
import com.tangem.tap.common.log.TangemCardSDKLogger
|
import com.tangem.tap.common.log.TangemCardSDKLogger
|
||||||
|
|
@ -17,10 +18,14 @@ internal object TangemLoggingModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideLoggingInitializer(appLogsStore: AppLogsStore): TangemLoggingInitializer {
|
fun provideLoggingInitializer(
|
||||||
|
appLogsStore: AppLogsStore,
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
|
): TangemLoggingInitializer {
|
||||||
return TangemLoggingInitializer(
|
return TangemLoggingInitializer(
|
||||||
appLogsStore = appLogsStore,
|
appLogsStore = appLogsStore,
|
||||||
tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
|
tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
|
||||||
|
environmentConfig = environmentConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,16 @@ internal object YieldSupplyDomainModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
|
||||||
|
): WrapYieldSwapCallDataWithUpgradeUseCase {
|
||||||
|
return WrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideYieldSupplyGetProtocolBalanceUseCase(
|
fun provideYieldSupplyGetProtocolBalanceUseCase(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -58,6 +57,7 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
||||||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
@ -73,8 +73,6 @@ internal class DefaultTangemSdkManager(
|
||||||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
|
||||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||||
private val cardRepository: CardRepository,
|
private val cardRepository: CardRepository,
|
||||||
) : TangemSdkManager {
|
) : TangemSdkManager {
|
||||||
|
|
@ -145,12 +143,10 @@ internal class DefaultTangemSdkManager(
|
||||||
runTaskAsyncReturnOnMain(
|
runTaskAsyncReturnOnMain(
|
||||||
runnable = ScanProductTask(
|
runnable = ScanProductTask(
|
||||||
card = null,
|
card = null,
|
||||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
|
||||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||||
visaCardScanHandler = visaCardScanHandler,
|
visaCardScanHandler = visaCardScanHandler,
|
||||||
visaCoroutineScope = this,
|
visaCoroutineScope = this,
|
||||||
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
||||||
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
|
||||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
),
|
),
|
||||||
|
|
@ -242,6 +238,7 @@ internal class DefaultTangemSdkManager(
|
||||||
Analytics.send(event = analyticsEvent.withParams(params.toMap()))
|
Analytics.send(event = analyticsEvent.withParams(params.toMap()))
|
||||||
}
|
}
|
||||||
.doOnFailure { tangemError ->
|
.doOnFailure { tangemError ->
|
||||||
|
TangemLogger.e("scanProduct failed: code=${tangemError.code}, message=${tangemError.customMessage}")
|
||||||
(tangemError as? TangemSdkError)?.let { error ->
|
(tangemError as? TangemSdkError)?.let { error ->
|
||||||
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
|
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +467,6 @@ internal class DefaultTangemSdkManager(
|
||||||
runnable = FinalizeTwinTask(
|
runnable = FinalizeTwinTask(
|
||||||
twinPublicKey = secondCardPublicKey,
|
twinPublicKey = secondCardPublicKey,
|
||||||
issuerKeys = issuerKeyPair,
|
issuerKeys = issuerKeyPair,
|
||||||
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
),
|
),
|
||||||
cardId = cardId,
|
cardId = cardId,
|
||||||
|
|
|
||||||
|
|
@ -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.Tlv
|
||||||
import com.tangem.common.tlv.TlvDecoder
|
import com.tangem.common.tlv.TlvDecoder
|
||||||
import com.tangem.crypto.CryptoUtils
|
import com.tangem.crypto.CryptoUtils
|
||||||
import com.tangem.crypto.hdWallet.DerivationPath
|
|
||||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||||
|
|
@ -32,25 +30,21 @@ import com.tangem.operations.PreflightReadMode
|
||||||
import com.tangem.operations.ScanTask
|
import com.tangem.operations.ScanTask
|
||||||
import com.tangem.operations.backup.PrimaryCard
|
import com.tangem.operations.backup.PrimaryCard
|
||||||
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
||||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
|
||||||
import com.tangem.operations.files.ReadFilesTask
|
import com.tangem.operations.files.ReadFilesTask
|
||||||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||||
import com.tangem.tap.domain.TapSdkError
|
import com.tangem.tap.domain.TapSdkError
|
||||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||||
import com.tangem.tap.mainScope
|
import com.tangem.tap.mainScope
|
||||||
import com.tangem.tap.scope
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class ScanProductTask(
|
internal class ScanProductTask(
|
||||||
private val card: Card?,
|
private val card: Card?,
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
|
||||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||||
private val visaCoroutineScope: CoroutineScope?,
|
private val visaCoroutineScope: CoroutineScope?,
|
||||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||||
private val shouldCheckIsAlreadyActivated: Boolean,
|
private val shouldCheckIsAlreadyActivated: Boolean,
|
||||||
private val isDynamicAddressesEnabled: Boolean,
|
|
||||||
private val cardRepository: CardRepository,
|
private val cardRepository: CardRepository,
|
||||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||||
) : CardSessionRunnable<ScanResponse> {
|
) : CardSessionRunnable<ScanResponse> {
|
||||||
|
|
@ -80,8 +74,6 @@ internal class ScanProductTask(
|
||||||
session = session,
|
session = session,
|
||||||
cardDto = cardDto,
|
cardDto = cardDto,
|
||||||
scanWalletProcessor = ScanWalletProcessor(
|
scanWalletProcessor = ScanWalletProcessor(
|
||||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
|
||||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
),
|
),
|
||||||
callback = callback,
|
callback = callback,
|
||||||
|
|
@ -92,8 +84,6 @@ internal class ScanProductTask(
|
||||||
val commandProcessor = when {
|
val commandProcessor = when {
|
||||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||||
else -> ScanWalletProcessor(
|
else -> ScanWalletProcessor(
|
||||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
|
||||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -102,8 +92,8 @@ internal class ScanProductTask(
|
||||||
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
|
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
|
||||||
when (scanTaskResult) {
|
when (scanTaskResult) {
|
||||||
is CompletionResult.Success -> {
|
is CompletionResult.Success -> {
|
||||||
// it needed because processorResult.data.card doesn't contains attestation result
|
// It's needed because processorResult.data.card doesn't contain the attestation
|
||||||
// and CardWallet.derivedKeys
|
// result or the existing CardWallet.derivedKeys read from the card.
|
||||||
val processorScanResponseWithNewCard = processorResult.data.copy(
|
val processorScanResponseWithNewCard = processorResult.data.copy(
|
||||||
card = CardDTO(scanTaskResult.data),
|
card = CardDTO(scanTaskResult.data),
|
||||||
)
|
)
|
||||||
|
|
@ -176,8 +166,6 @@ internal class ScanProductTask(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ScanWalletProcessor(
|
private class ScanWalletProcessor(
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
|
||||||
private val isDynamicAddressesEnabled: Boolean,
|
|
||||||
private val cardRepository: CardRepository,
|
private val cardRepository: CardRepository,
|
||||||
) : ProductCommandProcessor<ScanResponse> {
|
) : ProductCommandProcessor<ScanResponse> {
|
||||||
|
|
||||||
|
|
@ -281,48 +269,34 @@ private class ScanWalletProcessor(
|
||||||
when (linkingResult) {
|
when (linkingResult) {
|
||||||
is CompletionResult.Success -> {
|
is CompletionResult.Success -> {
|
||||||
primaryCard = linkingResult.data
|
primaryCard = linkingResult.data
|
||||||
deriveKeysIfNeeded(card, session, callback)
|
completeScan(card, session, callback)
|
||||||
}
|
}
|
||||||
is CompletionResult.Failure -> {
|
is CompletionResult.Failure -> {
|
||||||
deriveKeysIfNeeded(card, session, callback)
|
completeScan(card, session, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
deriveKeysIfNeeded(card, session, callback)
|
completeScan(card, session, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deriveKeysIfNeeded(
|
// Keys are no longer derived during scan: default derivations are created up front in
|
||||||
|
// CreateProductWalletTask, and derivations for additional tokens are handled by
|
||||||
|
// DefaultColdMapDerivationsRepository when the user explicitly adds a token.
|
||||||
|
private fun completeScan(
|
||||||
card: CardDTO,
|
card: CardDTO,
|
||||||
session: CardSession,
|
session: CardSession,
|
||||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val productType = getWalletProductType(card)
|
|
||||||
scope.launch {
|
|
||||||
val scanResponse = ScanResponse(
|
val scanResponse = ScanResponse(
|
||||||
card = card,
|
card = card,
|
||||||
productType = productType,
|
productType = getWalletProductType(card),
|
||||||
walletData = session.environment.walletData,
|
walletData = session.environment.walletData,
|
||||||
primaryCard = primaryCard,
|
primaryCard = primaryCard,
|
||||||
)
|
)
|
||||||
val derivations = collectDerivations(card, scanResponse)
|
|
||||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
|
||||||
callback(CompletionResult.Success(scanResponse))
|
callback(CompletionResult.Success(scanResponse))
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
|
|
||||||
when (result) {
|
|
||||||
is CompletionResult.Success -> {
|
|
||||||
val response = scanResponse.copy(derivedKeys = result.data.entries)
|
|
||||||
callback(CompletionResult.Success(response))
|
|
||||||
}
|
|
||||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWalletProductType(card: CardDTO): ProductType {
|
private fun getWalletProductType(card: CardDTO): ProductType {
|
||||||
|
|
@ -334,17 +308,6 @@ private class ScanWalletProcessor(
|
||||||
else -> ProductType.Wallet
|
else -> ProductType.Wallet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun collectDerivations(
|
|
||||||
card: CardDTO,
|
|
||||||
scanResponse: ScanResponse,
|
|
||||||
): Map<ByteArrayKey, List<DerivationPath>> {
|
|
||||||
val blockchains = blockchainToDeriveFinder
|
|
||||||
?.find(card)
|
|
||||||
?: return emptyMap()
|
|
||||||
|
|
||||||
return MissedDerivationsFinder(scanResponse, isDynamicAddressesEnabled).findByBlockchainsToDerive(blockchains)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||||
class FinalizeTwinTask(
|
class FinalizeTwinTask(
|
||||||
private val twinPublicKey: ByteArray,
|
private val twinPublicKey: ByteArray,
|
||||||
private val issuerKeys: KeyPair,
|
private val issuerKeys: KeyPair,
|
||||||
private val isDynamicAddressesEnabled: Boolean,
|
|
||||||
private val cardRepository: CardRepository,
|
private val cardRepository: CardRepository,
|
||||||
) : CardSessionRunnable<ScanResponse> {
|
) : CardSessionRunnable<ScanResponse> {
|
||||||
|
|
||||||
|
|
@ -31,11 +30,9 @@ class FinalizeTwinTask(
|
||||||
is CompletionResult.Success ->
|
is CompletionResult.Success ->
|
||||||
ScanProductTask(
|
ScanProductTask(
|
||||||
card = readResult.data,
|
card = readResult.data,
|
||||||
blockchainToDeriveFinder = null,
|
|
||||||
visaCardScanHandler = null,
|
visaCardScanHandler = null,
|
||||||
visaCoroutineScope = null,
|
visaCoroutineScope = null,
|
||||||
shouldCheckIsAlreadyActivated = false,
|
shouldCheckIsAlreadyActivated = false,
|
||||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
|
||||||
onboardingV2FeatureToggles = null,
|
onboardingV2FeatureToggles = null,
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
).run(session, callback)
|
).run(session, callback)
|
||||||
|
|
|
||||||
|
|
@ -325,11 +325,7 @@ internal class DefaultUserWalletsListRepository(
|
||||||
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
||||||
.doOnSuccess { sensitiveInfo ->
|
.doOnSuccess { sensitiveInfo ->
|
||||||
updateWallets { wallets ->
|
updateWallets { wallets ->
|
||||||
// It is necessary to update derivations because when scanning we obtain the missing keys
|
wallets?.updateWith(walletIdToSensitiveInformation = sensitiveInfo)
|
||||||
wallets?.updateWith(
|
|
||||||
walletIdToSensitiveInformation = sensitiveInfo,
|
|
||||||
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card)
|
trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
package com.tangem.tap.domain.userWalletList.utils
|
package com.tangem.tap.domain.userWalletList.utils
|
||||||
|
|
||||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||||
|
|
||||||
|
|
@ -74,10 +72,7 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
|
||||||
return this.map { it.toUserWallet() }
|
return this.map { it.toUserWallet() }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun UserWallet.updateWith(
|
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
|
||||||
sensitiveInformation: UserWalletSensitiveInformation,
|
|
||||||
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
|
|
||||||
): UserWallet {
|
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is UserWallet.Cold -> {
|
is UserWallet.Cold -> {
|
||||||
copy(
|
copy(
|
||||||
|
|
@ -85,7 +80,6 @@ internal fun UserWallet.updateWith(
|
||||||
card = scanResponse.card.copy(
|
card = scanResponse.card.copy(
|
||||||
wallets = requireNotNull(sensitiveInformation.wallets),
|
wallets = requireNotNull(sensitiveInformation.wallets),
|
||||||
),
|
),
|
||||||
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
|
|
||||||
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -98,17 +92,14 @@ internal fun UserWallet.updateWith(
|
||||||
|
|
||||||
internal fun List<UserWallet>.updateWith(
|
internal fun List<UserWallet>.updateWith(
|
||||||
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
||||||
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
|
|
||||||
): List<UserWallet> {
|
): List<UserWallet> {
|
||||||
return if (walletIdToSensitiveInformation.isEmpty()) {
|
return if (walletIdToSensitiveInformation.isEmpty()) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this.map { wallet ->
|
this.map { wallet ->
|
||||||
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
|
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
|
||||||
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
|
|
||||||
|
|
||||||
if (sensitiveInformation != null) {
|
if (sensitiveInformation != null) {
|
||||||
wallet.updateWith(sensitiveInformation, derivedKeys)
|
wallet.updateWith(sensitiveInformation)
|
||||||
} else {
|
} else {
|
||||||
wallet
|
wallet
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import com.tangem.feature.stories.api.StoriesComponent
|
||||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||||
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
||||||
import com.tangem.features.account.AccountCreateEditComponent
|
import com.tangem.features.account.AccountCreateEditComponent
|
||||||
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
|
|
||||||
import com.tangem.features.account.AccountDetailsComponent
|
import com.tangem.features.account.AccountDetailsComponent
|
||||||
import com.tangem.features.account.ArchivedAccountListComponent
|
import com.tangem.features.account.ArchivedAccountListComponent
|
||||||
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
|
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
|
||||||
|
|
@ -21,6 +20,7 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||||
import com.tangem.features.home.api.HomeComponent
|
import com.tangem.features.home.api.HomeComponent
|
||||||
import com.tangem.features.hotwallet.*
|
import com.tangem.features.hotwallet.*
|
||||||
import com.tangem.features.kyc.KycComponent
|
import com.tangem.features.kyc.KycComponent
|
||||||
|
import com.tangem.features.survey.SurveyComponent
|
||||||
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||||
|
|
@ -112,9 +112,9 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
||||||
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
|
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
|
||||||
private val kycComponentFactory: KycComponent.Factory,
|
private val kycComponentFactory: KycComponent.Factory,
|
||||||
|
private val surveyComponentFactory: SurveyComponent.Factory,
|
||||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||||
private val addFundsComponentFactory: AddFundsComponent.Factory,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
|
|
@ -216,6 +216,7 @@ internal class ChildFactory @Inject constructor(
|
||||||
userWalletId = route.userWalletId,
|
userWalletId = route.userWalletId,
|
||||||
cryptoCurrency = route.currency,
|
cryptoCurrency = route.currency,
|
||||||
source = route.source,
|
source = route.source,
|
||||||
|
initialFiatAmount = route.initialFiatAmount,
|
||||||
),
|
),
|
||||||
componentFactory = onrampComponentFactory,
|
componentFactory = onrampComponentFactory,
|
||||||
)
|
)
|
||||||
|
|
@ -234,13 +235,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
componentFactory = buyCryptoComponentFactory,
|
componentFactory = buyCryptoComponentFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppRoute.AddFunds -> {
|
|
||||||
createComponentChild(
|
|
||||||
context = context,
|
|
||||||
params = AddFundsComponent.Params(userWalletId = route.userWalletId),
|
|
||||||
componentFactory = addFundsComponentFactory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is AppRoute.SellCrypto -> {
|
is AppRoute.SellCrypto -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
@ -701,6 +695,13 @@ internal class ChildFactory @Inject constructor(
|
||||||
componentFactory = kycComponentFactory,
|
componentFactory = kycComponentFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is AppRoute.Survey -> {
|
||||||
|
createComponentChild(
|
||||||
|
context = context,
|
||||||
|
params = SurveyComponent.Params(token = route.token, displayId = route.displayId),
|
||||||
|
componentFactory = surveyComponentFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
is AppRoute.YieldSupplyEntry -> {
|
is AppRoute.YieldSupplyEntry -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
||||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||||
|
import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
|
||||||
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
||||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||||
|
|
@ -62,6 +63,7 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
private val newsDeepLink: NewsDeepLinkHandler.Factory,
|
private val newsDeepLink: NewsDeepLinkHandler.Factory,
|
||||||
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
||||||
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
|
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
|
||||||
|
private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
|
||||||
) {
|
) {
|
||||||
private val permittedAppRoute = MutableStateFlow(false)
|
private val permittedAppRoute = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
@ -175,6 +177,7 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
||||||
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
||||||
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
||||||
|
DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
|
||||||
else -> {
|
else -> {
|
||||||
TangemLogger.i(
|
TangemLogger.i(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHand
|
||||||
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
|
||||||
|
import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||||
|
|
@ -99,6 +100,10 @@ class DeepLinkFactoryTest {
|
||||||
every { create(any()) } returns mockk()
|
every { create(any()) } returns mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val surveyDeepLinkFactory = mockk<SurveyDeepLinkHandler.Factory>(relaxed = true) {
|
||||||
|
every { create(any()) } returns mockk()
|
||||||
|
}
|
||||||
|
|
||||||
private val earnDeepLinkFactory = mockk<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
private val earnDeepLinkFactory = mockk<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
||||||
every { create(any()) } returns mockk()
|
every { create(any()) } returns mockk()
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +145,7 @@ class DeepLinkFactoryTest {
|
||||||
newsDeepLink = newsDeepLinkFactory,
|
newsDeepLink = newsDeepLinkFactory,
|
||||||
earnDeepLink = earnDeepLinkFactory,
|
earnDeepLink = earnDeepLinkFactory,
|
||||||
yieldDeepLink = yieldDeepLinkFactory,
|
yieldDeepLink = yieldDeepLinkFactory,
|
||||||
|
surveyDeepLink = surveyDeepLinkFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,6 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Wallet : AppRoute(path = "/wallet")
|
data object Wallet : AppRoute(path = "/wallet")
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AddFunds(
|
|
||||||
val userWalletId: UserWalletId,
|
|
||||||
) : AppRoute(path = "/add_funds/${userWalletId.stringValue}")
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CurrencyDetails(
|
data class CurrencyDetails(
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
|
|
@ -297,6 +292,7 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
val source: OnrampSource,
|
val source: OnrampSource,
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
val currency: CryptoCurrency,
|
val currency: CryptoCurrency,
|
||||||
|
val initialFiatAmount: SerializedBigDecimal? = null,
|
||||||
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
|
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
|
||||||
override fun getBundle(): Bundle = bundle(serializer())
|
override fun getBundle(): Bundle = bundle(serializer())
|
||||||
}
|
}
|
||||||
|
|
@ -497,6 +493,9 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
|
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Survey(val token: String, val displayId: String? = null) : AppRoute(path = "/survey")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class YieldSupplyEntry(
|
data class YieldSupplyEntry(
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,10 @@ sealed class DeepLinkRoute {
|
||||||
data object PayAppMain : DeepLinkRoute() {
|
data object PayAppMain : DeepLinkRoute() {
|
||||||
override val host: String = "pay-app-main"
|
override val host: String = "pay-app-main"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data object Survey : DeepLinkRoute() {
|
||||||
|
override val host: String = "survey"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DeepLinkScheme(val scheme: String) {
|
enum class DeepLinkScheme(val scheme: String) {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
@Assisted private val onHandleQuickAction: (action: HandledQuickAction, shouldDismiss: Boolean) -> Unit,
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val messageSender: UiMessageSender,
|
private val messageSender: UiMessageSender,
|
||||||
) {
|
) {
|
||||||
|
|
@ -49,6 +49,18 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
action = action,
|
action = action,
|
||||||
cryptoCurrencyData = cryptoCurrencyData,
|
cryptoCurrencyData = cryptoCurrencyData,
|
||||||
),
|
),
|
||||||
|
when (action) {
|
||||||
|
TokenActionsBSContentUM.Action.Receive,
|
||||||
|
TokenActionsBSContentUM.Action.CopyAddress,
|
||||||
|
TokenActionsBSContentUM.Action.Sell,
|
||||||
|
-> false
|
||||||
|
TokenActionsBSContentUM.Action.Send,
|
||||||
|
TokenActionsBSContentUM.Action.Stake,
|
||||||
|
TokenActionsBSContentUM.Action.YieldMode,
|
||||||
|
TokenActionsBSContentUM.Action.Buy,
|
||||||
|
TokenActionsBSContentUM.Action.Exchange,
|
||||||
|
-> true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
val userWallet = cryptoCurrencyData.userWallet
|
val userWallet = cryptoCurrencyData.userWallet
|
||||||
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
|
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
|
||||||
|
|
@ -164,7 +176,7 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
currentAppCurrency: Provider<AppCurrency>,
|
currentAppCurrency: Provider<AppCurrency>,
|
||||||
onHandleQuickAction: (HandledQuickAction) -> Unit,
|
onHandleQuickAction: (HandledQuickAction, shouldDismiss: Boolean) -> Unit,
|
||||||
): TokenActionsHandler
|
): TokenActionsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,7 @@ import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -22,6 +16,7 @@ import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||||
import androidx.compose.ui.draw.blur
|
import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.innerShadow
|
import androidx.compose.ui.draw.innerShadow
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.shadow.Shadow
|
import androidx.compose.ui.graphics.shadow.Shadow
|
||||||
|
|
@ -36,22 +31,15 @@ import com.tangem.common.ui.earn.EarnBlockUM.Type
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.CircleShimmer
|
import com.tangem.core.ui.components.CircleShimmer
|
||||||
import com.tangem.core.ui.components.RectangleShimmer
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
import com.tangem.core.ui.ds.button.TangemButton
|
import com.tangem.core.ui.ds.button.*
|
||||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
|
||||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
|
||||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
|
||||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
|
||||||
import com.tangem.core.ui.ds.image.TangemIcon
|
import com.tangem.core.ui.ds.image.TangemIcon
|
||||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||||
import com.tangem.core.res.R as CoreResR
|
import com.tangem.core.res.R as CoreResR
|
||||||
|
|
||||||
private const val TINTED_BACKGROUND_ALPHA = 0.1f
|
private const val TINTED_BACKGROUND_ALPHA = 0.1f
|
||||||
|
|
@ -74,7 +62,7 @@ fun EarnBlock(state: EarnBlockUM, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EarnBlockLoading(modifier: Modifier = Modifier) {
|
private fun EarnBlockLoading(modifier: Modifier = Modifier) {
|
||||||
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
|
val shape = RoundedCornerShape(TangemTheme.dimens2.x5)
|
||||||
TangemRowContainer(
|
TangemRowContainer(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
|
|
@ -91,13 +79,13 @@ private fun EarnBlockLoading(modifier: Modifier = Modifier) {
|
||||||
RectangleShimmer(
|
RectangleShimmer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(TangemRowLayoutId.START_TOP)
|
.layoutId(TangemRowLayoutId.START_TOP)
|
||||||
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
|
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
|
||||||
radius = TangemTheme.dimens2.x2,
|
radius = TangemTheme.dimens2.x2,
|
||||||
)
|
)
|
||||||
RectangleShimmer(
|
RectangleShimmer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||||
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
|
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
|
||||||
radius = TangemTheme.dimens2.x2,
|
radius = TangemTheme.dimens2.x2,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -106,7 +94,7 @@ private fun EarnBlockLoading(modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Modifier) {
|
private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Modifier) {
|
||||||
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
|
val shape = RoundedCornerShape(TangemTheme.dimens2.x5)
|
||||||
|
|
||||||
val clickModifier = state.onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier
|
val clickModifier = state.onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier
|
||||||
|
|
||||||
|
|
@ -114,7 +102,7 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.then(clickModifier.backgroundModifier(state.type, state.backgroundUM, shape)),
|
.then(clickModifier.backgroundModifier(state.type, state.backgroundUM, shape)),
|
||||||
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
|
contentPadding = PaddingValues(all = TangemTheme.dimens2.x4),
|
||||||
content = {
|
content = {
|
||||||
EarnBlockIcon(
|
EarnBlockIcon(
|
||||||
type = state.type,
|
type = state.type,
|
||||||
|
|
@ -193,17 +181,23 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
|
||||||
}
|
}
|
||||||
is EarnBlockUM.TrailingUM.Balance -> {
|
is EarnBlockUM.TrailingUM.Balance -> {
|
||||||
if (!trailingUM.isBalanceHidden) {
|
if (!trailingUM.isBalanceHidden) {
|
||||||
|
val fiatModifier = Modifier.layoutId(TangemRowLayoutId.END_TOP).let {
|
||||||
|
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) else it
|
||||||
|
}
|
||||||
|
val cryptoModifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM).let {
|
||||||
|
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) else it
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = trailingUM.fiatValue.resolveAnnotatedReference(),
|
text = trailingUM.fiatValue.resolveAnnotatedReference(),
|
||||||
style = TangemTheme.typography2.bodySemibold16,
|
style = TangemTheme.typography2.bodySemibold16,
|
||||||
color = TangemTheme.colors2.text.neutral.primary,
|
color = TangemTheme.colors2.text.neutral.primary,
|
||||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
|
modifier = fiatModifier,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = trailingUM.cryptoValue.resolveReference(),
|
text = trailingUM.cryptoValue.resolveReference(),
|
||||||
style = TangemTheme.typography2.captionMedium12,
|
style = TangemTheme.typography2.captionMedium12,
|
||||||
color = TangemTheme.colors2.text.neutral.secondary,
|
color = TangemTheme.colors2.text.neutral.secondary,
|
||||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
|
modifier = cryptoModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +355,7 @@ private val EarnBlockUM.TitleUM.Style.textStyle: TextStyle
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
|
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodyMedium16
|
||||||
EarnBlockUM.TitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
EarnBlockUM.TitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,7 +363,7 @@ private val EarnBlockUM.SubtitleUM.Style.textStyle: TextStyle
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
|
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodyMedium16
|
||||||
EarnBlockUM.SubtitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
EarnBlockUM.SubtitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
@ -410,12 +404,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
|
||||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
|
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_stake),
|
text = resourceReference(CoreResR.string.common_stake),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Disabled,
|
tone = EarnBlockUM.TitleUM.Tone.Disabled,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||||
),
|
),
|
||||||
trailingUM = null,
|
trailingUM = null,
|
||||||
|
|
@ -426,12 +420,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_staking),
|
text = resourceReference(CoreResR.string.common_staking),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = stringReference("Average APR 5.24%"),
|
text = stringReference("Average APR 5.24%"),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||||
|
|
@ -445,12 +439,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.staking_enabled),
|
text = resourceReference(CoreResR.string.staking_enabled),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = stringReference("$ 12.34 rewards"),
|
text = stringReference("$ 12.34 rewards"),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Balance(
|
trailingUM = EarnBlockUM.TrailingUM.Balance(
|
||||||
|
|
@ -475,14 +469,14 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
id = CoreResR.string.yield_module_token_details_earn_notification_subtitle,
|
id = CoreResR.string.yield_module_token_details_earn_notification_subtitle,
|
||||||
formatArgs = wrappedList("5.24"),
|
formatArgs = wrappedList("5.24"),
|
||||||
),
|
),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = resourceReference(
|
text = resourceReference(
|
||||||
CoreResR.string.yield_module_token_details_earn_notification_description,
|
CoreResR.string.yield_module_token_details_earn_notification_description,
|
||||||
),
|
),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||||
|
|
@ -497,7 +491,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.yield_module_transaction_enter),
|
text = resourceReference(CoreResR.string.yield_module_transaction_enter),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
|
|
@ -505,7 +499,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
id = CoreResR.string.yield_module_average_apy,
|
id = CoreResR.string.yield_module_average_apy,
|
||||||
formatArgs = wrappedList("5.24"),
|
formatArgs = wrappedList("5.24"),
|
||||||
),
|
),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||||
|
|
@ -521,7 +515,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning),
|
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning),
|
||||||
),
|
),
|
||||||
|
|
@ -530,7 +524,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
id = CoreResR.string.yield_module_average_apy,
|
id = CoreResR.string.yield_module_average_apy,
|
||||||
formatArgs = wrappedList("5.24"),
|
formatArgs = wrappedList("5.24"),
|
||||||
),
|
),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||||
|
|
@ -546,7 +540,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info),
|
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info),
|
||||||
),
|
),
|
||||||
|
|
@ -555,7 +549,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
id = CoreResR.string.yield_module_average_apy,
|
id = CoreResR.string.yield_module_average_apy,
|
||||||
formatArgs = wrappedList("5.24"),
|
formatArgs = wrappedList("5.24"),
|
||||||
),
|
),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
),
|
),
|
||||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||||
|
|
@ -571,12 +565,12 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = resourceReference(CoreResR.string.common_enabling),
|
text = resourceReference(CoreResR.string.common_enabling),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||||
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Positive),
|
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Positive),
|
||||||
),
|
),
|
||||||
|
|
@ -589,12 +583,12 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
|
||||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_yield_disabling_40),
|
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_yield_disabling_40),
|
||||||
titleUM = EarnBlockUM.TitleUM(
|
titleUM = EarnBlockUM.TitleUM(
|
||||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||||
style = EarnBlockUM.TitleUM.Style.Large,
|
style = EarnBlockUM.TitleUM.Style.Small,
|
||||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||||
),
|
),
|
||||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||||
text = resourceReference(CoreResR.string.common_disabling),
|
text = resourceReference(CoreResR.string.common_disabling),
|
||||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||||
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Muted),
|
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Muted),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.common.ui.R
|
import com.tangem.common.ui.R
|
||||||
|
|
@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -78,7 +80,8 @@ private fun ExpressTransactionItem(
|
||||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
.background(TangemTheme.colors2.surface.level3)
|
.background(TangemTheme.colors2.surface.level3)
|
||||||
.clickable(onClick = info.onClick)
|
.clickable(onClick = info.onClick)
|
||||||
.padding(TangemTheme.dimens2.x4),
|
.padding(TangemTheme.dimens2.x4)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM),
|
||||||
) {
|
) {
|
||||||
TitleRow(
|
TitleRow(
|
||||||
title = info.title.resolveReference(),
|
title = info.title.resolveReference(),
|
||||||
|
|
@ -104,7 +107,9 @@ private fun TitleRow(title: String, infoIconRes: Int?, infoIconTint: Color?) {
|
||||||
text = title,
|
text = title,
|
||||||
style = TangemTheme.typography2.bodyMedium16,
|
style = TangemTheme.typography2.bodyMedium16,
|
||||||
color = TangemTheme.colors3.text.primary,
|
color = TangemTheme.colors3.text.primary,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TITLE),
|
||||||
)
|
)
|
||||||
if (infoIconRes != null && infoIconTint != null) {
|
if (infoIconRes != null && infoIconTint != null) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -126,32 +131,42 @@ private fun AmountsRow(info: ExpressTransactionStateInfoUM) {
|
||||||
CurrencyIcon(
|
CurrencyIcon(
|
||||||
state = info.fromCurrencyIcon,
|
state = info.fromCurrencyIcon,
|
||||||
shouldDisplayNetwork = false,
|
shouldDisplayNetwork = false,
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
modifier = Modifier
|
||||||
|
.size(TangemTheme.dimens.size18)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_ICON),
|
||||||
)
|
)
|
||||||
EllipsisText(
|
EllipsisText(
|
||||||
text = info.fromAmount.resolveReference(),
|
text = info.fromAmount.resolveReference(),
|
||||||
style = TangemTheme.typography2.bodyMedium16,
|
style = TangemTheme.typography2.bodyMedium16,
|
||||||
color = TangemTheme.colors3.text.primary,
|
color = TangemTheme.colors3.text.primary,
|
||||||
ellipsis = TextEllipsis.OffsetEnd(info.fromAmountSymbol.length),
|
ellipsis = TextEllipsis.OffsetEnd(info.fromAmountSymbol.length),
|
||||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_AMOUNT),
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_forward_24),
|
painter = painterResource(R.drawable.ic_forward_24),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = TangemTheme.colors3.icon.tertiary,
|
tint = TangemTheme.colors3.icon.tertiary,
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
modifier = Modifier
|
||||||
|
.size(TangemTheme.dimens.size18)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON),
|
||||||
)
|
)
|
||||||
CurrencyIcon(
|
CurrencyIcon(
|
||||||
state = info.toCurrencyIcon,
|
state = info.toCurrencyIcon,
|
||||||
shouldDisplayNetwork = false,
|
shouldDisplayNetwork = false,
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
modifier = Modifier
|
||||||
|
.size(TangemTheme.dimens.size18)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON),
|
||||||
)
|
)
|
||||||
EllipsisText(
|
EllipsisText(
|
||||||
text = info.toAmount.resolveReference(),
|
text = info.toAmount.resolveReference(),
|
||||||
style = TangemTheme.typography2.bodyMedium16,
|
style = TangemTheme.typography2.bodyMedium16,
|
||||||
color = TangemTheme.colors3.text.primary,
|
color = TangemTheme.colors3.text.primary,
|
||||||
ellipsis = TextEllipsis.OffsetEnd(info.toAmountSymbol.length),
|
ellipsis = TextEllipsis.OffsetEnd(info.toAmountSymbol.length),
|
||||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.semantics.disabled
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||||
|
|
@ -61,15 +64,14 @@ fun TokenActionRow(
|
||||||
val accentColor = accentColor(isEnabled)
|
val accentColor = accentColor(isEnabled)
|
||||||
TangemRowContainer(
|
TangemRowContainer(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(
|
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
|
||||||
color = TangemTheme.colors2.surface.level3,
|
.background(color = TangemTheme.colors2.surface.level3)
|
||||||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
|
||||||
)
|
|
||||||
.clickableWithHaptic(
|
.clickableWithHaptic(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
hapticManager = hapticManager,
|
hapticManager = hapticManager,
|
||||||
),
|
)
|
||||||
|
.semantics { if (!isEnabled) disabled() },
|
||||||
) {
|
) {
|
||||||
LeadingIcon(iconRes = iconRes, accentColor = accentColor)
|
LeadingIcon(iconRes = iconRes, accentColor = accentColor)
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,7 @@ sealed class AnalyticsParam {
|
||||||
const val WALLET_TYPE = "Wallet Type"
|
const val WALLET_TYPE = "Wallet Type"
|
||||||
const val BACKUPED = "Backuped"
|
const val BACKUPED = "Backuped"
|
||||||
const val MEMO = "Memo"
|
const val MEMO = "Memo"
|
||||||
|
const val VALUE = "Value"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "APP_REDESIGN_ENABLED",
|
"name": "APP_REDESIGN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "5.40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "GASLESS_APPROVAL_ENABLED",
|
"name": "GASLESS_APPROVAL_ENABLED",
|
||||||
|
|
@ -55,6 +55,10 @@
|
||||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "TWI_1326_YIELD_MODE_SWAP_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ADDRESS_SYNC_ENABLED",
|
"name": "ADDRESS_SYNC_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
@ -64,7 +68,7 @@
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SWAP_INTEGRATED_APPROVE",
|
"name": "AND_15120_SWAP_INTEGRATED_APPROVE",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -106,5 +110,21 @@
|
||||||
{
|
{
|
||||||
"name": "TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED",
|
"name": "TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED",
|
||||||
"version": "5.39"
|
"version": "5.39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AND_15482_SURVEYSPARROW_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AND_15368_VISA_PAY_REDESIGN",
|
||||||
|
"version": "undefined"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.core.configtoggle.feature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature toggle information exposed by [MutableFeatureTogglesManager].
|
||||||
|
*
|
||||||
|
* @property name raw toggle name
|
||||||
|
* @property version release version from local config ("undefined" for permanently disabled toggles)
|
||||||
|
* @property isEnabled current toggle state (may differ from default if overridden locally)
|
||||||
|
*/
|
||||||
|
data class FeatureToggleInfo(
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -2,6 +2,9 @@ package com.tangem.core.configtoggle.feature
|
||||||
|
|
||||||
import com.tangem.core.configtoggle.FeatureToggles
|
import com.tangem.core.configtoggle.FeatureToggles
|
||||||
|
|
||||||
|
/** Version value marking a feature toggle that has no planned release (permanently disabled). */
|
||||||
|
const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for getting information about the availability of feature toggles
|
* Component for getting information about the availability of feature toggles
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ interface MutableFeatureTogglesManager : FeatureTogglesManager {
|
||||||
/** Check if the current state of the feature toggles matches the local config state. */
|
/** Check if the current state of the feature toggles matches the local config state. */
|
||||||
fun isMatchLocalConfig(): Boolean
|
fun isMatchLocalConfig(): Boolean
|
||||||
|
|
||||||
/** Get feature toggles */
|
/** Get feature toggles with version info */
|
||||||
fun getFeatureToggles(): Map<String, Boolean>
|
fun getFeatureToggles(): List<FeatureToggleInfo>
|
||||||
|
|
||||||
/** Change availability [isEnabled] of toggle with name [name] */
|
/** Change availability [isEnabled] of toggle with name [name] */
|
||||||
suspend fun changeToggle(name: String, isEnabled: Boolean)
|
suspend fun changeToggle(name: String, isEnabled: Boolean)
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@ package com.tangem.core.configtoggle.feature.impl
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.tangem.core.configtoggle.FeatureToggles
|
import com.tangem.core.configtoggle.FeatureToggles
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureToggleInfo
|
||||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||||
import com.tangem.core.configtoggle.utils.toTableString
|
import com.tangem.core.configtoggle.utils.toTableString
|
||||||
import com.tangem.core.configtoggle.version.VersionProvider
|
import com.tangem.core.configtoggle.version.VersionProvider
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature toggles manager implementation in dev or mocked build
|
* Feature toggles manager implementation in dev or mocked build
|
||||||
|
|
@ -24,54 +26,62 @@ internal class DevFeatureTogglesManager(
|
||||||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||||
) : MutableFeatureTogglesManager {
|
) : MutableFeatureTogglesManager {
|
||||||
|
|
||||||
private val fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
private val fileFeatureToggles: List<FeatureToggleInfo> = buildFileFeatureToggles()
|
||||||
|
|
||||||
@Suppress("DoubleMutabilityForCollection")
|
private val currentToggles: MutableStateFlow<List<FeatureToggleInfo>> = MutableStateFlow(buildInitialToggles())
|
||||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
|
||||||
|
|
||||||
init {
|
override fun isFeatureEnabled(toggle: FeatureToggles): Boolean =
|
||||||
val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
|
currentToggles.value.any { it.name == toggle.rawName && it.isEnabled }
|
||||||
|
|
||||||
featureTogglesMap = fileFeatureTogglesMap
|
|
||||||
.mapValues { resultToggle ->
|
|
||||||
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
|
||||||
}
|
|
||||||
.toMutableMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFeatureEnabled(toggle: FeatureToggles): Boolean = featureTogglesMap[toggle.rawName] == true
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
fun isFeatureEnabledByName(name: String): Boolean = featureTogglesMap[name] == true
|
fun isFeatureEnabledByName(name: String): Boolean = currentToggles.value.any { it.name == name && it.isEnabled }
|
||||||
|
|
||||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
override fun getFeatureToggles(): List<FeatureToggleInfo> = currentToggles.value
|
||||||
|
|
||||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == fileFeatureTogglesMap
|
override fun isMatchLocalConfig(): Boolean =
|
||||||
|
currentToggles.value.associateBy { it.name } == fileFeatureToggles.associateBy { it.name }
|
||||||
|
|
||||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||||
featureTogglesMap[name] ?: return
|
if (currentToggles.value.none { it.name == name }) return
|
||||||
featureTogglesMap[name] = isEnabled
|
currentToggles.update { toggles ->
|
||||||
featureTogglesLocalStorage.store(value = featureTogglesMap)
|
toggles.map { toggle ->
|
||||||
|
if (toggle.name == name) toggle.copy(isEnabled = isEnabled) else toggle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
featureTogglesLocalStorage.store(value = currentToggles.value.toAvailabilityMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun recoverLocalConfig() {
|
override suspend fun recoverLocalConfig() {
|
||||||
featureTogglesMap = fileFeatureTogglesMap.toMutableMap()
|
currentToggles.value = fileFeatureToggles
|
||||||
featureTogglesLocalStorage.store(value = fileFeatureTogglesMap)
|
featureTogglesLocalStorage.store(value = currentToggles.value.toAvailabilityMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
|
return currentToggles.value.toAvailabilityMap()
|
||||||
}
|
.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
|
||||||
|
|
||||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
|
||||||
val appVersion = versionProvider.get()
|
|
||||||
|
|
||||||
return featureTogglesProvider.getToggles()
|
|
||||||
.defineTogglesAvailability(appVersion = appVersion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||||
featureTogglesMap = map
|
currentToggles.value = map.map { (name, isEnabled) ->
|
||||||
|
val version = fileFeatureToggles.firstOrNull { it.name == name }?.version.orEmpty()
|
||||||
|
FeatureToggleInfo(name = name, version = version, isEnabled = isEnabled)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInitialToggles(): List<FeatureToggleInfo> {
|
||||||
|
val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||||
|
return fileFeatureToggles.map { it.copy(isEnabled = savedFeatureToggles[it.name] ?: it.isEnabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFileFeatureToggles(): List<FeatureToggleInfo> {
|
||||||
|
val rawToggles = featureTogglesProvider.getToggles()
|
||||||
|
val availability = rawToggles.defineTogglesAvailability(appVersion = versionProvider.get())
|
||||||
|
return rawToggles.map { (name, version) ->
|
||||||
|
FeatureToggleInfo(name = name, version = version, isEnabled = availability.getValue(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<FeatureToggleInfo>.toAvailabilityMap(): Map<String, Boolean> =
|
||||||
|
associate { it.name to it.isEnabled }
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package com.tangem.core.configtoggle.feature.impl
|
|
||||||
|
|
||||||
internal object FeatureTogglesConstants {
|
|
||||||
|
|
||||||
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,7 @@ import com.tangem.utils.logging.TangemLogger
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
internal class Version private constructor(value: String) : Comparable<Version> {
|
class Version private constructor(value: String) : Comparable<Version> {
|
||||||
|
|
||||||
private val major: Int
|
private val major: Int
|
||||||
private val minor: Int
|
private val minor: Int
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.tangem.core.configtoggle.version
|
package com.tangem.core.configtoggle.version
|
||||||
|
|
||||||
|
import com.tangem.core.configtoggle.feature.DISABLED_FEATURE_TOGGLE_VERSION
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version contract to evaluate availability of feature toggle
|
* Version contract to evaluate availability of feature toggle
|
||||||
*
|
*
|
||||||
|
|
@ -7,8 +9,6 @@ package com.tangem.core.configtoggle.version
|
||||||
*/
|
*/
|
||||||
internal object VersionAvailabilityContract {
|
internal object VersionAvailabilityContract {
|
||||||
|
|
||||||
private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined"
|
|
||||||
|
|
||||||
/** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */
|
/** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */
|
||||||
operator fun invoke(currentVersion: String, localVersion: String): Boolean {
|
operator fun invoke(currentVersion: String, localVersion: String): Boolean {
|
||||||
if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false
|
if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ internal class FeatureTogglesNamingConventionTest {
|
||||||
"SOLANA_TX_HISTORY_ENABLED",
|
"SOLANA_TX_HISTORY_ENABLED",
|
||||||
"STAKING_ETH_ENABLED",
|
"STAKING_ETH_ENABLED",
|
||||||
"SWAP_AB_ENABLED",
|
"SWAP_AB_ENABLED",
|
||||||
"SWAP_INTEGRATED_APPROVE",
|
|
||||||
"USEDESK_ENABLED",
|
"USEDESK_ENABLED",
|
||||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||||
"VISA_ONBOARDING_ENABLED",
|
"VISA_ONBOARDING_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.core.configtoggle.manager
|
package com.tangem.core.configtoggle.manager
|
||||||
|
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureToggleInfo
|
||||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||||
|
|
@ -28,6 +29,14 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
version != "undefined" && !appVersion.isNullOrEmpty()
|
version != "undefined" && !appVersion.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Boolean>.toToggleInfoList(): List<FeatureToggleInfo> = map { (name, isEnabled) ->
|
||||||
|
FeatureToggleInfo(
|
||||||
|
name = name,
|
||||||
|
version = testToggles.getValue(name),
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
inner class Initialization {
|
inner class Initialization {
|
||||||
|
|
@ -60,7 +69,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = savedToggles
|
val expected = savedToggles
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -86,7 +95,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = savedToggles
|
val expected = savedToggles
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -112,7 +121,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = savedToggles
|
val expected = savedToggles
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -159,7 +168,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = getExpectedFileToggles(appVersion)
|
val expected = getExpectedFileToggles(appVersion)
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -185,7 +194,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = fileToggles
|
val expected = fileToggles
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -302,6 +311,59 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isMatchLocalConfig is true when toggles match but order differs`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { versionProvider.get() } returns "1.0.0"
|
||||||
|
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||||
|
|
||||||
|
// Same toggles and values as the local config, but in reversed order
|
||||||
|
val reorderedToggles = getExpectedFileToggles(appVersion = "1.0.0")
|
||||||
|
.entries.reversed()
|
||||||
|
.associate { it.key to it.value }
|
||||||
|
.toMutableMap()
|
||||||
|
val manager = DevFeatureTogglesManager(
|
||||||
|
versionProvider,
|
||||||
|
featureTogglesProvider,
|
||||||
|
featureTogglesLocalStorage,
|
||||||
|
).apply {
|
||||||
|
setFeatureToggles(reorderedToggles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = manager.isMatchLocalConfig()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Truth.assertThat(actual).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isMatchLocalConfig is false when a value differs despite reversed order`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { versionProvider.get() } returns "1.0.0"
|
||||||
|
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||||
|
|
||||||
|
// Reversed order AND one toggle value flipped → must not match
|
||||||
|
val reorderedChangedToggles = getExpectedFileToggles(appVersion = "1.0.0")
|
||||||
|
.entries.reversed()
|
||||||
|
.associate { it.key to it.value }
|
||||||
|
.toMutableMap()
|
||||||
|
.apply { this["ENABLED_TOGGLE"] = false }
|
||||||
|
val manager = DevFeatureTogglesManager(
|
||||||
|
versionProvider,
|
||||||
|
featureTogglesProvider,
|
||||||
|
featureTogglesLocalStorage,
|
||||||
|
).apply {
|
||||||
|
setFeatureToggles(reorderedChangedToggles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = manager.isMatchLocalConfig()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Truth.assertThat(actual).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
private fun provideTestModels(): List<IsMatchLocalConfigModel> {
|
private fun provideTestModels(): List<IsMatchLocalConfigModel> {
|
||||||
val appVersion = "1.0.0"
|
val appVersion = "1.0.0"
|
||||||
val fileToggles = getExpectedFileToggles(appVersion)
|
val fileToggles = getExpectedFileToggles(appVersion)
|
||||||
|
|
@ -369,7 +431,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = fileToggles + savedToggles
|
val expected = fileToggles + savedToggles
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -405,7 +467,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
val actual = manager.getFeatureToggles()
|
val actual = manager.getFeatureToggles()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(model.expectedToggles)
|
Truth.assertThat(actual).containsExactlyElementsIn(model.expectedToggles.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
@ -478,7 +540,7 @@ internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expected = getExpectedFileToggles(appVersion)
|
val expected = getExpectedFileToggles(appVersion)
|
||||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
Truth.assertThat(actual).containsExactlyElementsIn(expected.toToggleInfoList())
|
||||||
|
|
||||||
coVerifyOrder {
|
coVerifyOrder {
|
||||||
versionProvider.get()
|
versionProvider.get()
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ dependencies {
|
||||||
implementation(deps.kotlin.coroutines)
|
implementation(deps.kotlin.coroutines)
|
||||||
implementation(deps.kotlin.coroutines.rx2)
|
implementation(deps.kotlin.coroutines.rx2)
|
||||||
implementation(deps.kotlin.datetime)
|
implementation(deps.kotlin.datetime)
|
||||||
|
implementation(deps.kotlin.serialization)
|
||||||
|
|
||||||
/** Logging */
|
/** Logging */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.tangem.datasource.api.auth
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.auth.models.request.AuthApiRequest
|
||||||
|
import com.tangem.datasource.api.auth.models.request.NonceApiRequest
|
||||||
|
import com.tangem.datasource.api.auth.models.request.RefreshApiRequest
|
||||||
|
import com.tangem.datasource.api.auth.models.response.NonceApiResponse
|
||||||
|
import com.tangem.datasource.api.auth.models.response.TokenApiResponse
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tangem Auth Service API (JWT session tokens / DPoP interceptor / refresh rotation)
|
||||||
|
*/
|
||||||
|
interface AuthApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request authentication nonce.
|
||||||
|
*
|
||||||
|
* Generates a nonce bound to the device public key for the authentication flow.
|
||||||
|
*/
|
||||||
|
@POST("api/v1/auth/nonce/auth")
|
||||||
|
suspend fun requestAuthNonce(@Body request: NonceApiRequest): ApiResponse<NonceApiResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate device.
|
||||||
|
*
|
||||||
|
* Authenticates a previously registered device using a device-key signature. Issues a new
|
||||||
|
* JWT access token with bound `walletIds[]` and risk tier. All subsequent auth after
|
||||||
|
* registration uses this endpoint.
|
||||||
|
*/
|
||||||
|
@POST("api/v1/auth/authenticate")
|
||||||
|
suspend fun authenticate(@Body request: AuthApiRequest): ApiResponse<TokenApiResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh tokens.
|
||||||
|
*
|
||||||
|
* Rotates the refresh token and issues a new access token. Uses refresh-token rotation
|
||||||
|
* with family-based reuse detection — replaying a consumed token revokes the entire token
|
||||||
|
* family (SR-8). Sender-constraint is verified via the DPoP-proof header (`cnf.jkt`).
|
||||||
|
*/
|
||||||
|
@POST("api/v1/auth/refresh")
|
||||||
|
@RequiresSessionAuth
|
||||||
|
suspend fun refresh(@Body request: RefreshApiRequest): ApiResponse<TokenApiResponse>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.tangem.datasource.api.auth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a Retrofit endpoint as requiring an authenticated session (DPoP, see
|
||||||
|
* [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)).
|
||||||
|
*
|
||||||
|
* Read at runtime by the session-auth interceptor: only methods
|
||||||
|
* carrying this annotation receive `Authorization: DPoP <access-token>` + `DPoP: <proof-jwt>`
|
||||||
|
* headers; unannotated methods (e.g. public nonce endpoints) pass through unchanged.
|
||||||
|
*
|
||||||
|
* Mirrors the per-operation `security` blocks in the backend OpenAPI contract; follows the
|
||||||
|
* same on-method annotation pattern as `@ReadTimeout` / `@ConnectTimeout`.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class RequiresSessionAuth
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.tangem.datasource.api.auth.models.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/** Authentication request — authenticates a previously registered device. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AuthApiRequest(
|
||||||
|
/** Signed authentication payload. */
|
||||||
|
@Json(name = "payload") val payload: AuthenticationPayload,
|
||||||
|
/** EC signature over the authentication payload, signed by the device private key (Base64). */
|
||||||
|
@Json(name = "signature") val signature: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Signed authentication payload — the data that is signed by the device private key. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AuthenticationPayload(
|
||||||
|
/** Base64-encoded EC public key of the device (e.g. `MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...`). */
|
||||||
|
@Json(name = "devicePublicKey") val devicePublicKey: String,
|
||||||
|
/** Deciphered nonce value from the nonce endpoint. */
|
||||||
|
@Json(name = "nonce") val nonce: String,
|
||||||
|
/** Platform attestation token (Play Integrity / App Attest). */
|
||||||
|
@Json(name = "attestationToken") val attestationToken: String?,
|
||||||
|
/** Client-reported device metadata. */
|
||||||
|
@Json(name = "metadata") val metadata: DeviceMetadata,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** Device metadata collection. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class DeviceMetadata(
|
||||||
|
/** Device hardware model (e.g. `iPhone 15 Pro`). */
|
||||||
|
@Json(name = "deviceModel") val deviceModel: String?,
|
||||||
|
/** Operating system (`android` / `ios`). */
|
||||||
|
@Json(name = "os") val os: String,
|
||||||
|
/** OS version string (e.g. `17.4.1`). */
|
||||||
|
@Json(name = "osVersion") val osVersion: String?,
|
||||||
|
/** Application version (e.g. `5.8.0`). */
|
||||||
|
@Json(name = "appVersion") val appVersion: String?,
|
||||||
|
/** User-Agent header (e.g. `Tangem/5.8.0 (iPhone; iOS 17.4.1; Scale/3.00)`). */
|
||||||
|
@Json(name = "userAgent") val userAgent: String?,
|
||||||
|
/** Client locale (e.g. `en-US`). */
|
||||||
|
@Json(name = "locale") val locale: String?,
|
||||||
|
/** Client timezone (e.g. `Europe/Moscow`). */
|
||||||
|
@Json(name = "timezone") val timezone: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.tangem.datasource.api.auth.models.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/** Request body for nonce generation (auth, upgrade, wallet flows). */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class NonceApiRequest(
|
||||||
|
/** Base64-encoded EC public key of the device (e.g. `MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...`). */
|
||||||
|
@Json(name = "devicePublicKey") val devicePublicKey: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.tangem.datasource.api.auth.models.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/** Token refresh request. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RefreshApiRequest(
|
||||||
|
/** Refresh token from a previous token response. */
|
||||||
|
@Json(name = "refreshToken") val refreshToken: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.datasource.api.auth.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/** Ciphered nonce response. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class NonceApiResponse(
|
||||||
|
/** RSA-OAEP ciphered nonce value (Base64). */
|
||||||
|
@Json(name = "cipheredNonce") val cipheredNonce: String,
|
||||||
|
/** Nonce expiration timestamp (ISO-8601). */
|
||||||
|
@Json(name = "expiresAt") val expiresAt: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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?,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.datasource.api.auth.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/** Token response — contains JWT access token and optional refresh token. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TokenApiResponse(
|
||||||
|
/** JWT access token (HMAC-SHA256 signed). */
|
||||||
|
@Json(name = "accessToken") val accessToken: String,
|
||||||
|
/** Access token expiration timestamp (ISO-8601). */
|
||||||
|
@Json(name = "accessTokenExpiresAt") val accessTokenExpiresAt: String,
|
||||||
|
/** Refresh token for token rotation. `null` for ORANGE tier (requires device challenge each time). */
|
||||||
|
@Json(name = "refreshToken") val refreshToken: String?,
|
||||||
|
/** Refresh token expiration timestamp (ISO-8601). `null` iff [refreshToken] is `null`. */
|
||||||
|
@Json(name = "refreshTokenExpiresAt") val refreshTokenExpiresAt: String?,
|
||||||
|
/** List of wallet IDs bound to this device. */
|
||||||
|
@Json(name = "walletIds") val walletIds: List<String>,
|
||||||
|
)
|
||||||
|
|
@ -33,6 +33,7 @@ sealed class ApiConfig {
|
||||||
News,
|
News,
|
||||||
GaslessTxService,
|
GaslessTxService,
|
||||||
SurveySparrow,
|
SurveySparrow,
|
||||||
|
Auth,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeId(): ID {
|
private fun initializeId(): ID {
|
||||||
|
|
@ -49,6 +50,7 @@ sealed class ApiConfig {
|
||||||
is News -> ID.News
|
is News -> ID.News
|
||||||
is GaslessTxService -> ID.GaslessTxService
|
is GaslessTxService -> ID.GaslessTxService
|
||||||
is SurveySparrow -> ID.SurveySparrow
|
is SurveySparrow -> ID.SurveySparrow
|
||||||
|
is Auth -> ID.Auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
|
import com.tangem.datasource.BuildConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tangem Auth Service [ApiConfig] — endpoints for device registration, authentication,
|
||||||
|
* nonce issuance, refresh token rotation, and JWKS publication.
|
||||||
|
*/
|
||||||
|
internal class Auth : ApiConfig() {
|
||||||
|
|
||||||
|
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||||
|
|
||||||
|
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||||
|
createDevEnvironment(),
|
||||||
|
createMockedEnvironment(),
|
||||||
|
createProdEnvironment(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getInitialEnvironment(): ApiEnvironment {
|
||||||
|
return when (BuildConfig.BUILD_TYPE) {
|
||||||
|
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||||
|
DEBUG_BUILD_TYPE,
|
||||||
|
INTERNAL_BUILD_TYPE,
|
||||||
|
-> ApiEnvironment.DEV
|
||||||
|
EXTERNAL_BUILD_TYPE,
|
||||||
|
RELEASE_BUILD_TYPE,
|
||||||
|
-> ApiEnvironment.PROD
|
||||||
|
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||||
|
environment = ApiEnvironment.DEV,
|
||||||
|
baseUrl = DEV_BASE_URL,
|
||||||
|
headers = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||||
|
environment = ApiEnvironment.MOCK,
|
||||||
|
baseUrl = MOCK_BASE_URL,
|
||||||
|
headers = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||||
|
environment = ApiEnvironment.PROD,
|
||||||
|
baseUrl = PROD_BASE_URL,
|
||||||
|
headers = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
// TODO Replace with real Auth Service hosts once the backend team confirms deployment.
|
||||||
|
// Swagger currently only declares `http://localhost:8080` for local development.
|
||||||
|
// [REDACTED_JIRA]
|
||||||
|
private const val DEV_BASE_URL = "http://localhost:8080/"
|
||||||
|
private const val MOCK_BASE_URL = "http://localhost:8080/"
|
||||||
|
private const val PROD_BASE_URL = "http://localhost:8080/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,6 +97,12 @@ interface TangemPayApi {
|
||||||
@Body body: ReissueCardRequest,
|
@Body body: ReissueCardRequest,
|
||||||
): ApiResponse<ReissueCardResponse>
|
): ApiResponse<ReissueCardResponse>
|
||||||
|
|
||||||
|
@POST("v1/customer/card/close")
|
||||||
|
suspend fun closeCard(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Body body: CloseCardRequest,
|
||||||
|
): ApiResponse<CloseCardResponse>
|
||||||
|
|
||||||
@POST("v1/customer/card/withdraw/data")
|
@POST("v1/customer/card/withdraw/data")
|
||||||
suspend fun getWithdrawData(
|
suspend fun getWithdrawData(
|
||||||
@Header("Authorization") authHeader: String,
|
@Header("Authorization") authHeader: String,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -113,4 +113,10 @@ internal object ApiConfigsModule {
|
||||||
fun provideSurveySparrowConfig(environmentConfig: EnvironmentConfig): ApiConfig {
|
fun provideSurveySparrowConfig(environmentConfig: EnvironmentConfig): ApiConfig {
|
||||||
return SurveySparrow(environmentConfig)
|
return SurveySparrow(environmentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@IntoSet
|
||||||
|
fun provideAuthConfig(): ApiConfig {
|
||||||
|
return Auth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.datasource.di
|
package com.tangem.datasource.di
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
|
import com.tangem.datasource.api.auth.AuthApi
|
||||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||||
import com.tangem.datasource.api.surveysparrow.SurveySparrowApi
|
import com.tangem.datasource.api.surveysparrow.SurveySparrowApi
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig
|
import com.tangem.datasource.api.common.config.ApiConfig
|
||||||
|
|
@ -208,6 +209,15 @@ internal object NetworkModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAuthApi(retrofitApiBuilder: RetrofitApiBuilder): AuthApi {
|
||||||
|
return retrofitApiBuilder.build(
|
||||||
|
apiConfigId = ApiConfig.ID.Auth,
|
||||||
|
applyTimeoutAnnotations = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGaslessTxServiceApi(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApi {
|
fun provideGaslessTxServiceApi(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApi {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package com.tangem.datasource.di
|
||||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
|
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
|
||||||
|
import com.tangem.datasource.local.visa.DefaultTangemPayCloseCardStore
|
||||||
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
|
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
|
||||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||||
|
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
|
||||||
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
|
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -32,4 +34,12 @@ internal object TangemPayStoresModule {
|
||||||
prefs = prefs,
|
prefs = prefs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideTangemPayCloseCardStore(prefs: AppPreferencesStore): TangemPayCloseCardStore {
|
||||||
|
return DefaultTangemPayCloseCardStore(
|
||||||
|
prefs = prefs,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -26,6 +27,11 @@ internal object ConfigModule {
|
||||||
return GeneratedEnvironmentConfigConverter.convert()
|
return GeneratedEnvironmentConfigConverter.convert()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("authServiceKey")
|
||||||
|
fun provideAuthServiceKey(environmentConfig: EnvironmentConfig): String? = environmentConfig.authServiceKey
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
|
fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,15 @@ import com.tangem.datasource.api.utils.ConnectTimeout
|
||||||
import com.tangem.datasource.api.utils.ReadTimeout
|
import com.tangem.datasource.api.utils.ReadTimeout
|
||||||
import com.tangem.datasource.api.utils.WriteTimeout
|
import com.tangem.datasource.api.utils.WriteTimeout
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
|
import com.tangem.datasource.local.logs.SensitiveUrlMasker
|
||||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
import com.tangem.datasource.utils.addHeaders
|
import com.tangem.datasource.utils.addHeaders
|
||||||
|
import com.tangem.utils.JsonStringValuesExtractor
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Invocation
|
import retrofit2.Invocation
|
||||||
|
|
@ -41,6 +45,7 @@ import javax.inject.Singleton
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class RetrofitApiBuilder @Inject constructor(
|
internal class RetrofitApiBuilder @Inject constructor(
|
||||||
private val apiConfigs: ApiConfigs,
|
private val apiConfigs: ApiConfigs,
|
||||||
|
|
@ -49,10 +54,20 @@ internal class RetrofitApiBuilder @Inject constructor(
|
||||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val appLogsStore: AppLogsStore,
|
private val appLogsStore: AppLogsStore,
|
||||||
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
|
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
|
||||||
|
|
||||||
|
private val sensitiveUrlMasker: SensitiveUrlMasker by lazy {
|
||||||
|
val json = Json.encodeToJsonElement(EnvironmentConfig.serializer(), environmentConfig)
|
||||||
|
// Drop URL-shaped values (e.g. public endpoint URLs from config); they are not secrets
|
||||||
|
// and would obscure unrelated requests in logs.
|
||||||
|
val values = JsonStringValuesExtractor.extract(json)
|
||||||
|
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }
|
||||||
|
SensitiveUrlMasker(values)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a Retrofit API instance for the specified API configuration ID
|
* Builds a Retrofit API instance for the specified API configuration ID
|
||||||
*
|
*
|
||||||
|
|
@ -179,7 +194,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder {
|
private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder {
|
||||||
return addInterceptor(
|
return addInterceptor(
|
||||||
interceptor = NetworkLogsSaveInterceptor(appLogsStore),
|
interceptor = NetworkLogsSaveInterceptor(appLogsStore, sensitiveUrlMasker),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||||
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
||||||
import com.tangem.datasource.local.config.environment.models.SurveySparrowSwapRatingConfig
|
import com.tangem.datasource.local.config.environment.models.SurveySparrowSwapRatingConfig
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class EnvironmentConfig(
|
data class EnvironmentConfig(
|
||||||
val moonPayApiKey: String = "",
|
val moonPayApiKey: String = "",
|
||||||
val moonPayApiSecretKey: String = "",
|
val moonPayApiSecretKey: String = "",
|
||||||
|
|
@ -32,5 +35,7 @@ data class EnvironmentConfig(
|
||||||
val gaslessTxApiKey: String? = null,
|
val gaslessTxApiKey: String? = null,
|
||||||
val customerIoCdpApiKey: String? = null,
|
val customerIoCdpApiKey: String? = null,
|
||||||
val surveySparrowToken: String? = null,
|
val surveySparrowToken: String? = null,
|
||||||
|
@Transient
|
||||||
val surveySparrowSwapRating: SurveySparrowSwapRatingConfig? = null,
|
val surveySparrowSwapRating: SurveySparrowSwapRatingConfig? = null,
|
||||||
|
val authServiceKey: String? = null,
|
||||||
)
|
)
|
||||||
|
|
@ -56,6 +56,7 @@ internal object GeneratedEnvironmentConfigConverter {
|
||||||
customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.androidApiKey,
|
customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.androidApiKey,
|
||||||
surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey,
|
surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey,
|
||||||
surveySparrowSwapRating = createSurveySparrowSwapRating(),
|
surveySparrowSwapRating = createSurveySparrowSwapRating(),
|
||||||
|
authServiceKey = null, // TODO: provide service key [REDACTED_JIRA]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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