Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-19 11:44:37 +03:00
commit 1146554992
1461 changed files with 62266 additions and 17096 deletions

View file

@ -0,0 +1,320 @@
---
name: cleanup-feature-toggles
description: Remove released feature toggles (version <= target) — deletes from config, removes toggle properties, inlines `true` in calling code, removes dead branches. CI-safe, no prompts.
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent
argument-hint: <version> [--dry-run] [--only <TOGGLE_NAME>]
---
Remove all feature toggles whose version is less than or equal to the target release version.
**CRITICAL: This skill runs on CI. NEVER ask questions. If anything is ambiguous, make the safer choice or skip the toggle.**
## Constants
- **Config file**: `core/config-toggles/src/main/assets/configs/feature_toggles_config.json`
- **Generated enum** (DO NOT edit): `core/config-toggles/build/generated/source/toggles/com/tangem/core/configtoggle/FeatureToggles.kt`
- **Dry-run mode**: check if `$ARGUMENTS` contains `--dry-run`. In dry-run mode, make NO file changes — only output what WOULD be removed (including affected files and usage sites).
- **Version**: extract the version number from `$ARGUMENTS` (e.g., `5.35`, `5.35.0`). The version is the first argument that matches a semver-like pattern (`X.Y` or `X.Y.Z`).
- **Only mode**: check if `$ARGUMENTS` contains `--only <TOGGLE_NAME>`. If present, process ONLY the specified toggle (it must still satisfy the version check). Multiple `--only` flags can be provided.
## Phase 0: Preflight Checks
### 0a. Parse Arguments
Extract `<version>`, optional `--dry-run`, and optional `--only <TOGGLE_NAME>` (repeatable) from `$ARGUMENTS`.
- If no version found: STOP with `FATAL: No version provided. Usage: /cleanup-feature-toggles <version> [--dry-run] [--only <TOGGLE_NAME>]`
- Validate version matches pattern `\d+\.\d+(\.\d+)?` — if not, STOP with `FATAL: Invalid version format.`
- Normalize version: if only `X.Y` is given, treat as `X.Y.0` for comparison.
- If `--only` flags are present, collect the toggle names into a filter list.
### 0b. Verify Git State
```bash
git status --porcelain 2>&1
```
- If output is empty (clean working tree) — OK.
- If there are uncommitted changes — STOP with: `FATAL: Working tree is not clean. Commit or stash changes before running this skill.`
Initialize an internal results list to track each toggle's outcome.
## Phase 1: Identify Toggles to Remove
1. Read `core/config-toggles/src/main/assets/configs/feature_toggles_config.json`.
2. For each toggle entry in the JSON array:
- If `version == "undefined"` → skip (unreleased feature, must not be removed).
- Parse the toggle's version as semver (normalize `X.Y` to `X.Y.0`).
- If toggle version **<=** target version → mark for removal.
3. If `--only` filter is active: keep only toggles whose `name` matches one of the `--only` values. If a `--only` toggle doesn't satisfy the version check, output a warning but still skip it.
4. Output the list of toggles marked for removal with their versions.
5. If no toggles match → output `No toggles to remove for version <version>` and stop.
6. If `--dry-run` mode → proceed to Phase 2 (research only, all toggles in parallel), then skip to Phase 7 to output the detailed summary. Do NOT make any file changes.
## Phase 2: Research (parallel)
Collect all information about all toggles **in parallel** before making any edits. Launch one `Agent` per toggle (all in a single message so they run concurrently). Each agent receives the toggle name and must return a structured report.
**Error handling rule**: if research fails for a toggle, record the failure reason and continue. Do NOT stop processing.
### Per-toggle research task (runs inside each Agent)
Each Agent performs the following read-only searches and returns a structured report:
#### 2a. Find the toggle property declaration and direct usages
Use `Grep` to search for `FeatureToggles.<TOGGLE_NAME>` (e.g., `FeatureToggles.WALLET_REORDER_FEATURE_ENABLED`) across the **entire** codebase.
This will find:
1. **`DefaultXxxFeatureToggles` property** — the standard wrapper. Extract:
- The **property name** (e.g., `isWalletReorderFeatureEnabled`)
- The **DefaultXxxFeatureToggles file path**
- The **XxxFeatureToggles interface name** (from the class's supertype)
2. **Direct `FeatureTogglesManager.isFeatureEnabled()` calls** — code that bypasses the wrapper and calls the manager directly. These are additional usage sites.
Also find the interface file:
- Use `Glob` to find the `XxxFeatureToggles.kt` file in `features/*/api/` or `core/*/`
Check if the toggle property or the `DefaultXxxFeatureToggles` class has **comments referencing additional cleanup** (e.g., `// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle`). If found, record the comment text.
If the toggle reference is not found anywhere: report as `Skipped (no property found)`.
#### 2b. Find `@RemoveWithToggle` annotated code
Use `Grep` to search for `RemoveWithToggle` (without `@` or package prefix) across the entire codebase (excluding the annotation definition itself). Then filter matches to only those where the `toggleName` argument equals the current toggle name.
The annotation is defined in `core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt` (`com.tangem.utils.annotations.RemoveWithToggle`). It has two parameters: `toggleName: String` (the toggle name) and `description: String` (optional hint).
Support all Kotlin annotation forms:
- `@RemoveWithToggle("TOGGLE_NAME")`
- `@RemoveWithToggle(toggleName = "TOGGLE_NAME")`
- `@com.tangem.utils.annotations.RemoveWithToggle("TOGGLE_NAME")`
- `@com.tangem.utils.annotations.RemoveWithToggle(toggleName = "TOGGLE_NAME")`
For each filtered match, record:
- The file path and line number
- The annotated element name (class, function, property)
- The `description` value if present
#### 2c. Find all usages of the property in calling code
Use `Grep` to search for the property name (e.g., `isWalletReorderFeatureEnabled`) across the entire codebase.
Categorize results:
- **Interface declaration** — the `val isX: Boolean` in `XxxFeatureToggles.kt`
- **Implementation** — the `override val isX` in `DefaultXxxFeatureToggles.kt`
- **Calling code** — any other file that reads `*.isX` (include file path, line number, and the matched line content)
#### Agent report format
Each Agent must return a report with:
- Toggle name
- Property name (e.g., `isWalletReorderFeatureEnabled`) or `null` if not found
- Interface name and file path
- Implementation file path
- List of calling code sites: `[{file, line, content}]`
- List of direct `FeatureTogglesManager` usage sites: `[{file, line, content}]`
- List of `@RemoveWithToggle` sites: `[{file, line, element, description}]`
- Cleanup comments (if any)
- Status: `ready` or `skipped (reason)`
### After all Agents complete
Collect all reports. If `--dry-run` → skip to Phase 6 with the collected data.
## Phase 3: Edit (sequential)
Process each toggle **sequentially** using the research data from Phase 2. Only toggles with status `ready` are processed.
**Error handling rule**: if ANY step fails for a toggle, record the failure reason and continue to the next toggle. Do NOT stop processing.
### Step 3a: Replace usages in calling code with `true` and simplify
For each calling code usage site (from Phase 2 report), `Read` the surrounding context (at least 20 lines around the usage) and apply the appropriate simplification:
| Pattern | Simplification |
|---------|---------------|
| `if (toggles.isX) { body }` | Remove `if`, keep `body` (unindent) |
| `if (toggles.isX) { A } else { B }` | Keep only `A`, remove if/else structure |
| `if (!toggles.isX) { body }` | Remove entire if-block |
| `if (!toggles.isX) { A } else { B }` | Keep only `B`, remove if/else structure |
| `toggles.isX && expr` | Replace with `expr` |
| `expr && toggles.isX` | Replace with `expr` |
| `toggles.isX \|\| expr` | Replace with `true` (or simplify enclosing condition since it's always true) |
| `val x = toggles.isX` | Replace with `val x = true`, then check if `x` is used in one of the patterns above and simplify transitively |
| `property = toggles.isX` | Replace with `property = true` |
| `when { toggles.isX -> A; else -> B }` | Keep only `A`, remove the `when` structure |
| `when { !toggles.isX -> A; else -> B }` | Keep only `B`, remove the `when` structure |
| `when(value) { ... }` with toggle in a branch condition | Evaluate the toggle to `true`, simplify the `when` accordingly |
| Complex boolean expression | Replace `toggles.isX` with `true` and algebraically simplify |
Also process any direct `FeatureTogglesManager.isFeatureEnabled()` call sites the same way (replace with `true` and simplify).
**After replacing**, check if the file still references the `XxxFeatureToggles` type:
- If not → remove the import of `XxxFeatureToggles`
- If the type was a constructor/inject parameter and is no longer used → remove the parameter and any `@Inject`/`@Assisted` annotations associated with it
- If removing a constructor parameter from a Decompose Model or Component, also remove it from the caller that creates the instance
**Important**: Use `Edit` for precise changes. Read enough context to make correct edits. Do NOT accidentally delete unrelated code.
### Step 3b: Remove the property from interface and implementation
1. **In `XxxFeatureToggles` interface**: remove the `val isPropertyName: Boolean` line.
2. **In `DefaultXxxFeatureToggles`**: remove the `override val isPropertyName: Boolean` property (including the `get() = ...` line).
3. Check if `DefaultXxxFeatureToggles` still has other properties:
- If **yes** → done with this toggle.
- If **no** (all properties removed) → the interface and implementation are now empty. Check the **protected list** below — if the interface is protected, keep it and skip deletion. Otherwise, delete them:
**Protected interfaces (never delete even if empty):**
- `TokensFeatureToggles`
- `BlockchainSDKFeatureToggles`
- `StakingFeatureToggles`
- `CardSdkFeatureToggles`
- `TangemPayFeatureToggles`
- `SwapFeatureToggles`
- `SendFeatureToggles`
If the interface is **protected**: remove the `featureTogglesManager` / `featureToggles` constructor parameter from `DefaultXxxFeatureToggles`, remove unused imports (`FeatureTogglesManager`, `FeatureToggles`), but keep both files.
If the interface is **not protected** → delete them:
1. Delete the `XxxFeatureToggles` interface file.
2. Delete the `DefaultXxxFeatureToggles` implementation file.
3. Find and remove the Hilt binding for this interface (typically a `@Binds` method in a `*FeatureTogglesModule` or similar Hilt module). If the Hilt module has no remaining bindings after removal, delete the module file as well.
4. Use `Grep` to find all remaining references to `XxxFeatureToggles` and `DefaultXxxFeatureToggles` across the codebase. For each reference:
- **Constructor/inject parameter** → remove the parameter. If the surrounding class/function no longer uses any feature toggles, cascade the removal to its callers.
- **Import statement** → remove it.
- **Any other reference** → assess and remove or update as needed.
Record status as `Removed` with the count of usage sites simplified.
## Phase 4: Update JSON Config
1. Read `feature_toggles_config.json`.
2. Remove all entries whose `name` matches a successfully removed toggle (status = `Removed`).
3. Write back the JSON with proper formatting:
- 2-space indentation
- Each entry on its own lines
- No trailing commas
- Match the existing file format exactly
## Phase 5: Build, Test & Lint Verification
Run all verification tasks in a **single Gradle invocation** to avoid repeated cold starts:
### 5a. Build + Tests + Detekt
```bash
./gradlew assembleGoogleDebug unitTest detekt detektMain :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
```
- If the command **fails**:
- Read the error output to determine which task failed.
- **Compilation error** (`assembleGoogleDebug` or `assembleGoogleMocked`): attempt to fix (one retry — usually missing import removal or unused parameter). If still fails: revert all changes with `git checkout -- .` and output `FATAL: Build failed after cleanup. All changes reverted.` with the error details.
- **Unit test failure**: attempt to fix (one retry). If still fails: output the failures as warnings in the summary but do NOT revert.
- **Detekt violation**: attempt to fix (one retry — usually unused imports or parameters). If still fails: output the violations as warnings in the summary.
- **UI test compilation failure**: attempt to fix (one retry). If still fails: output the failures as warnings in the summary.
- After fixing, re-run the **full command** to verify everything passes together.
## Phase 6: Branch, Commit, Push & PR
Skip this phase entirely in `--dry-run` mode.
### 6a. Create Branch and Commit
```bash
git checkout -b tech/cleanup-toggles-<version>
git add -A
git commit -m "[Tech] Remove feature toggles <= <version>"
```
Replace `<version>` with the target version (e.g., `tech/cleanup-toggles-5.35`).
### 6b. Push
```bash
git push -u origin tech/cleanup-toggles-<version>
```
### 6c. Create Pull Request
Use `gh pr create` targeting `develop`:
```bash
gh pr create --base develop --title "Remove feature toggles <= <version>" --body "$(cat <<'EOF'
## Summary
Automated cleanup of feature toggles that are permanently enabled (version <= <version>).
### Removed toggles
- `TOGGLE_NAME_1` (version)
- `TOGGLE_NAME_2` (version)
- ...
### Manual review required
<List all @RemoveWithToggle-annotated elements found in Phase 2 research (file path, element name, description) and any cleanup comments. If none found, write "None">
## Test plan
- [x] `assembleGoogleDebug` passes
- [x] `unitTest` passes
- [x] `detekt detektMain` passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
Output the PR URL.
## Phase 7: Output Summary
Output results as a Markdown table:
```markdown
## Feature Toggle Cleanup Summary
| Toggle | Version | Interface | Usages Simplified | Status |
|--------|---------|-----------|-------------------|--------|
| WALLET_REORDER_FEATURE_ENABLED | 5.34 | WalletFeatureToggles | 3 | Removed |
| EARN_BLOCK_ENABLED | 5.35 | EarnFeatureToggles | 1 | Removed |
| SOME_TOGGLE | 5.33 | SomeFeatureToggles | — | Skipped (no property found) |
**Total:** X toggles processed, Y removed, Z skipped/failed
**Target version:** <version>
```
### Manual Review Hints
If any toggle had a comment referencing additional cleanup (found in Phase 2 research), output a separate section:
```markdown
### Manual Review Required
- **GASLESS_APPROVAL_ENABLED**: `// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle`
- **OTHER_TOGGLE**: `// Also remove legacy FooBar component`
```
### Dry-run mode output
In `--dry-run` mode: prepend `[DRY RUN]` to the header, set all statuses to `Would remove`, and add a detailed section per toggle:
```markdown
### WALLET_REORDER_FEATURE_ENABLED (5.34) — Would remove
**Property:** `WalletFeatureToggles.isWalletReorderFeatureEnabled`
**Files affected:**
- `features/details/impl/.../UserWalletListModel.kt:42``walletFeatureToggles.isWalletReorderFeatureEnabled && userWallets.size > 1`
- `features/wallet/impl/.../SomeOtherFile.kt:88``if (walletFeatureToggles.isWalletReorderFeatureEnabled)`
```
### Warnings
If unit tests or detekt failed after fix attempts, list the remaining issues:
```markdown
### Warnings
- **Unit test failure:** `:features:wallet:impl:testDebugUnitTest` — WalletModelTest.someTest (may need manual update)
- **Detekt violation:** UnusedPrivateMember in `SomeFile.kt:15`
```

View file

@ -73,6 +73,15 @@ android {
}
}
// `src/prodDi/` holds production DI bindings for interfaces with a `mocked` counterpart.
// Wired into every build type EXCEPT `mocked`, which supplies its own bindings from `src/mocked/`.
buildTypes.configureEach {
if (name != "mocked") {
sourceSets.named(name) {
java.srcDir("src/prodDi/java")
}
}
}
}
configurations.all {
@ -161,7 +170,7 @@ dependencies {
implementation(projects.domain.hotWallet)
implementation(projects.domain.news)
implementation(projects.domain.earn)
implementation(projects.domain.tokensync)
implementation(projects.domain.assetsdiscovery)
implementation(projects.domain.search)
implementation(projects.common)
@ -192,7 +201,7 @@ dependencies {
implementation(projects.data.common)
implementation(projects.data.settings)
implementation(projects.data.tokens)
implementation(projects.data.tokensync)
implementation(projects.data.assetsdiscovery)
implementation(projects.data.txhistory)
implementation(projects.data.wallets)
implementation(projects.data.analytics)
@ -276,6 +285,8 @@ dependencies {
implementation(projects.features.nft.impl)
implementation(projects.features.walletconnect.api)
implementation(projects.features.walletconnect.impl)
implementation(projects.features.commonFeatures.api)
implementation(projects.features.commonFeatures.impl)
implementation(projects.features.usedesk.api)
implementation(projects.features.usedesk.impl)
implementation(projects.features.hotWallet.api)
@ -375,7 +386,6 @@ dependencies {
implementation(deps.googlePlay.services)
implementation(deps.googlePlay.advertising)
coreLibraryDesugaring(deps.desugar)
implementation(deps.kermit)
implementation(deps.zxing.qrCore)
implementation(deps.coil)
implementation(deps.coil.gif)
@ -394,10 +404,8 @@ dependencies {
implementation(deps.viewBindingDelegate)
implementation(deps.armadillo)
implementation(deps.kotlin.serialization)
implementation(deps.reKotlin)
implementation(deps.reownCore)
implementation(deps.reownWeb3)
implementation(deps.prettyLogger)
implementation(deps.decompose.ext.compose)
implementation(deps.moshi.adapters)
implementation(deps.moshi.kotlin)

Binary file not shown.

View file

@ -8,6 +8,8 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport
import com.kaspersky.components.composesupport.config.addComposeSupport
@ -20,13 +22,15 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import io.qameta.allure.kotlin.Allure
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@ -58,6 +62,12 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var promoRepository: PromoRepository
@Inject
lateinit var walletManagersStore: WalletManagersStore
@Inject
lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -72,7 +82,11 @@ abstract class BaseTestCase : TestCase(
private val semanticTreePrinterRule = object : TestWatcher() {
override fun failed(e: Throwable?, description: Description?) {
runCatching { printAllRoots() }
runCatching {
runBlocking {
withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() }
}
}
}
}
@ -116,6 +130,12 @@ abstract class BaseTestCase : TestCase(
value = false
)
}
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.set(
key = PreferencesKeys.getShouldShowInitialPermissionScreen(PUSH_PERMISSION),
value = false
)
}
promoRepository.setNeverToShowWalletPromo(PromoId.Sepa)
}
apiEnvironmentRule.setup(apiConfigsManager)
@ -168,11 +188,13 @@ abstract class BaseTestCase : TestCase(
"ACCOUNTS_FEATURE_ENABLED" to true,
"GASLESS_APPROVAL_ENABLED" to true,
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
)
)
}
private companion object {
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L
}
}

View file

@ -32,9 +32,12 @@ object TestConstants {
const val DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
const val TERRA_RECIPIENT_ADDRESS = "terra148dmp5ccazcwdmrcpvqz5rprnn886kemqen3tj"
const val POLYGON_RECIPIENT_ADDRESS = "0x742d35cc6634c0532925a3b844bc9e7595f2bd18"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L
const val HOLD_DURATION_MS = 2_000L
const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN"
@ -42,5 +45,17 @@ object TestConstants {
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val REFERRAL_API_SCENARIO = "referral_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
"hawk when"
const val SEED_PHRASE_18 = "crush idle include refuse expose kiss slot budget uphold when dinner certain holiday " +
"slow word armor butter suffer"
const val SEED_PHRASE_21 = "employ space oval venue wash clog zebra cover icon wash assist word debris inform " +
"cable meadow add game meat rigid pride"
const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " +
"bread much nature basic fun iron benefit egg error prosper"
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
}

View file

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

View file

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

View file

@ -0,0 +1,102 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.accounts.onArchivedAccountsScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onMainScreenTopBar
import com.tangem.screens.onWalletSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openWalletSettingsScreen() {
step("Open 'Wallet details' screen") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
}
fun BaseTestCase.openAccountDetails(accountName: String) {
step("Click on account: '$accountName'") {
onWalletSettingsScreen { accountItem(accountName).clickWithAssertion() }
}
step("Assert 'Account details' screen is displayed") {
onAccountDetailsScreen { screenContainer.assertIsDisplayed() }
}
}
fun BaseTestCase.archiveAccount() {
step("Assert 'Archive' button is displayed") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
step("Click on 'Archive' button") {
onAccountDetailsScreen { archiveAccountButton.clickWithAssertion() }
}
step("Confirm archivation in dialog") {
onDialog { archiveButton.clickWithAssertion() }
}
}
fun BaseTestCase.assertArchiveConfirmationDialog() {
step("Assert confirmation dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert confirmation dialog has 'Cancel' button") {
onDialog { cancelButton.assertIsDisplayed() }
}
step("Assert confirmation dialog has 'Archive' button") {
onDialog { archiveButton.assertIsDisplayed() }
}
}
fun BaseTestCase.assertErrorDialog(expectedTitle: String, expectedMessage: String) {
step("Assert error dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert error dialog has proper title") {
onDialog {
title.assertTextContains(expectedTitle)
}
}
step("Assert error dialog has explanatory text") {
onDialog {
text.assertTextContains(expectedMessage)
}
}
step("Assert error dialog has 'OK' button") {
onDialog { okButton.assertIsDisplayed() }
}
}
fun BaseTestCase.dismissErrorDialog() {
step("Dismiss error dialog by clicking 'Ok' button") {
onDialog { okButton.clickWithAssertion() }
}
}
fun BaseTestCase.openArchivedAccountsScreen() {
step("Click on 'Archived accounts' button") {
onWalletSettingsScreen { openArchivedAccountsButton.clickWithAssertion() }
}
}
fun BaseTestCase.assertArchivedAccountIsDisplayed(accountName: String) {
step("Assert archived account with name '$accountName' is displayed") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(accountName)
.container.assertIsDisplayed()
}
}
}
fun BaseTestCase.restoreArchivedAccount(accountName: String) {
step("Restore account with name '$accountName'") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(accountName)
.restoreButton.clickWithAssertion()
}
}
}

View file

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

View file

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

View file

@ -72,8 +72,8 @@ fun BaseTestCase.checkSingleCurrencyMainScreen(
onMainScreen { emptyTransactionBlockExploreButton.assertIsDisplayed() }
}
}
step("Assert 'Organize tokens' button is not displayed") {
onMainScreen { organizeTokensButtonWithoutLazySearch.assertIsNotDisplayed() }
step("Assert 'Add & Manage' button is not displayed") {
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
}
}
@ -112,8 +112,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Receive' button is not displayed") {
onMainScreen { receiveButton.assertIsNotDisplayed() }
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButton().assertIsDisplayed() }
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.screens.onAddAndManageBottomSheet
import com.tangem.screens.onMainScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openOrganizeTokensScreen() {
step("Swipe to 'Add & Manage' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click on 'Add & Manage' button") {
onMainScreen { addAndManageButton().clickWithAssertion() }
}
step("Click on 'Organize tokens' button in bottom sheet") {
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeMarketsBlock
import com.tangem.common.extensions.swipeVertical
import com.tangem.screens.onMainScreen
import com.tangem.screens.onMarketsExchangesScreen
@ -55,7 +54,7 @@ fun BaseTestCase.openMarketsScreen() {
synchronizeAddresses()
}
step("Open 'Markets' screen") {
swipeMarketsBlock(SwipeDirection.UP)
onMainScreen { searchThroughMarketPlaceholder.performClick() }
waitForIdle()
}
}

View file

@ -1,18 +1,23 @@
package com.tangem.scenarios
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
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.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.assertIsDimmed
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import com.tangem.tap.domain.sdk.mocks.MockContent
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openSendScreen(tokenName: String, mockState: String = "") {
fun BaseTestCase.openSendScreen(
tokenName: String,
mockState: String = "",
mockContent: MockContent? = null,
) {
val scenarioState = mockState.ifEmpty { tokenName }
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
@ -21,7 +26,7 @@ fun BaseTestCase.openSendScreen(tokenName: String, mockState: String = "") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
openMainScreen(mockContent = mockContent)
}
step("Synchronize addresses") {
synchronizeAddresses()
@ -72,8 +77,10 @@ fun BaseTestCase.openSendConfirmScreen(
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
step("Click 'Next' button until 'Send Confirm' screen opens") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
runCatching { openSendConfirmScreenViaNextButton() }.isSuccess
}
}
}
@ -84,6 +91,9 @@ fun BaseTestCase.openSendAddressScreen(
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Assert 'Send' button is not dimmed") {
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
@ -182,3 +192,69 @@ fun BaseTestCase.openSendConfirmScreenViaNextButton() {
onSendConfirmScreen { sendButton.assertIsDisplayed() }
}
}
fun BaseTestCase.openSendSuccessScreenViaLongClickOnSendButton() {
step("Long click on 'Send' button") {
onSendConfirmScreen {
waitForIdle()
sendButton.assertIsEnabled()
sendButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
}
}
step("Assert 'Transaction sent' screen is displayed") {
onSendSuccessScreen { container.assertIsDisplayed() }
}
}
fun BaseTestCase.checkSendViaSwapSuccessScreen() {
step("Assert 'Transaction sent' title is displayed") {
onSendSuccessScreen { title.assertIsDisplayed() }
}
step("Assert 'Transaction date' is displayed") {
onSendSuccessScreen { transactionDate.assertIsDisplayed() }
}
step("Assert 'Send from' block is displayed") {
onSendSuccessScreen { sendFromBlock.assertIsDisplayed() }
}
step("Assert 'Amount to receive' block is displayed") {
onSendSuccessScreen { sendToAmountBlock.assertIsDisplayed() }
}
step("Assert 'Provider' block is displayed") {
onSendSuccessScreen { providerBlock.assertIsDisplayed() }
}
step("Assert 'Recipient address' block is displayed") {
onSendSuccessScreen { recipientAddressBlock.assertIsDisplayed() }
}
step("Assert 'Network fee' block is displayed") {
onSendSuccessScreen { feeBlock.assertIsDisplayed() }
}
step("Assert 'Explore' button is displayed") {
onSendSuccessScreen { exploreButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onSendSuccessScreen { shareButton.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onSendSuccessScreen { closeButton.assertIsDisplayed() }
}
}
fun BaseTestCase.selectTokenToSendViaSwap(
swapTokenName: String,
networkName: String,
networkType: String? = null,
) {
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Click on 'Swap to another token' button") {
onSendScreen { swapToAnotherTokenButton.performClick() }
}
step("Click on token: '$swapTokenName'") {
onSendViaSwapScreen { tokenItem(swapTokenName).performClick() }
}
val networkLabel = if (networkType.isNullOrBlank()) networkName else "$networkName $networkType"
step("Click on '$networkLabel' network") {
onChooseNetworkBottomSheet { networkItem(networkName, networkType).performClick() }
}
}

View file

@ -11,20 +11,17 @@ import io.qameta.allure.kotlin.Allure.step
import com.tangem.common.ui.R as CommonUiR
private val firstStoryIndex = 0
private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title)
private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle)
private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title_v2)
private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle_v2)
private val secondStoryIndex = 1
private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title)
private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle)
private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title_v2)
private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle_v2)
private val thirdStoryIndex = 2
private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title)
private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle)
private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title_v2)
private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle_v2)
private val forthStoryIndex = 3
private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title)
private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle)
private val fifthStoryIndex = 4
private val fifthStoryTitle = getResourceString(CommonUiR.string.swap_story_fifth_title)
private val fifthStorySubtitle = getResourceString(CommonUiR.string.swap_story_fifth_subtitle)
private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title_v2)
private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle_v2)
fun BaseTestCase.openSwapScreen(
from: SwapEntryPoint,
@ -120,26 +117,6 @@ fun BaseTestCase.checkStoriesChanges() {
storySubtitle = forthStorySubtitle
)
}
step("Click on right side") {
onSwapStoriesScreen { container.performTouchInput { click(centerRight) } }
}
step("Check title and subtitle for story №${fifthStoryIndex + 1}") {
checkStoriesContent(
storyIndex = fifthStoryIndex,
storyTitle = fifthStoryTitle,
storySubtitle = fifthStorySubtitle
)
}
step("Click on left side") {
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
}
step("Check title and subtitle for story №${forthStoryIndex + 1}") {
checkStoriesContent(
storyIndex = forthStoryIndex,
storyTitle = forthStoryTitle,
storySubtitle = forthStorySubtitle
)
}
step("Click on left side") {
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
}
@ -297,6 +274,15 @@ fun BaseTestCase.checkSwapWarning(
}
}
fun BaseTestCase.chooseReceiveToken(tokenName: String) {
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
step("Click on token with name '$tokenName'") {
onSwapSelectTokenScreen { tokenWithName(tokenName).performClick() }
}
}
sealed class SwapEntryPoint {
object MainScreen : SwapEntryPoint()
object TokenDetails : SwapEntryPoint()

View file

@ -0,0 +1,31 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
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 com.tangem.core.res.R as CoreResR
class AddAndManageBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddAndManageBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
useUnmergedTree = true
}
val addTokensButton: KNode = child {
hasText(getResourceString(CoreResR.string.add_and_manage_sheet_manage_title))
useUnmergedTree = true
}
val organizeTokensButton: KNode = child {
hasText(getResourceString(CoreResR.string.add_and_manage_sheet_organize_title))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onAddAndManageBottomSheet(function: AddAndManageBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -20,10 +20,12 @@ class ChooseNetworkBottomSheetPageObject(semanticsProvider: SemanticsNodeInterac
useUnmergedTree = true
}
fun networkItem(title: String, subtitle: String): KNode = child {
fun networkItem(name: String, type: String? = null): KNode = child {
hasTestTag(ChooseNetworkBottomSheetTestTags.NETWORK_ITEM)
hasAnyDescendant(withText(title))
hasAnyDescendant(withText(subtitle))
hasAnyDescendant(withText(name))
if (!type.isNullOrBlank()) {
hasAnyDescendant(withText(type))
}
useUnmergedTree = true
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
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 com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R as CoreUiR
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
@ -214,11 +215,29 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
}
val organizeTokensButtonNode: KNode = child {
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
val addAndManageButtonNode: KNode = child {
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
useUnmergedTree = true
}
/**
* Main account header on the main screen. Click to expand/collapse its tokens list.
*/
fun mainAccount(): LazyListItemNode = accountWithName(getResourceString(CoreUiR.string.account_main_account_title))
/**
* Account header on the main screen, located by its visible name. Click to expand/collapse its tokens list.
* The account's title text lives on a descendant of the test-tagged node, so we match by descendant.
*/
@OptIn(ExperimentalTestApi::class)
fun accountWithName(name: String): LazyListItemNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasAnyDescendant(withText(name))
useUnmergedTree = true
}
}
/**
* Find token list item with title and address
*/
@ -227,6 +246,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
useUnmergedTree = true
@ -238,6 +258,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON)
useUnmergedTree = true
@ -245,18 +266,18 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
@OptIn(ExperimentalTestApi::class)
fun organizeTokensButton(): KNode {
fun addAndManageButton(): KNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
}.child<KNode> {
hasText(getResourceString(R.string.organize_tokens_title))
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
useUnmergedTree = true
}
}
val organizeTokensButtonWithoutLazySearch: KNode = child {
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
hasText(getResourceString(R.string.organize_tokens_title))
val addAndManageButtonWithoutLazySearch: KNode = child {
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
useUnmergedTree = true
}
@ -279,6 +300,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
hasLazyListItemPosition(index)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
useUnmergedTree = true

View file

@ -0,0 +1,77 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TransactionSuccessScreenTestTags
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 SendSuccessPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendSuccessPageObject>(semanticsProvider = semanticsProvider) {
val container: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.TITLE)
useUnmergedTree = true
}
val transactionDate: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.TRANSACTION_DATE)
useUnmergedTree = true
}
val sendFromBlock: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.AMOUNT_BLOCK)
hasAnyDescendant(withText(getResourceString(R.string.send_from_title)))
useUnmergedTree = true
}
val sendToAmountBlock: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.AMOUNT_BLOCK)
hasAnyDescendant(
withText(getResourceString(R.string.send_with_swap_recipient_amount_success_title))
)
useUnmergedTree = true
}
val providerBlock: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.PROVIDER_BLOCK)
useUnmergedTree = true
}
val recipientAddressBlock: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.RECIPIENT_BLOCK)
useUnmergedTree = true
}
val feeBlock: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.FEE_BLOCK)
useUnmergedTree = true
}
val exploreButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_explore))
}
val closeButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_close))
}
val shareButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_share))
}
}
internal fun BaseTestCase.onSendSuccessScreen(function: SendSuccessPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,60 +0,0 @@
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.AppBarWithSearchTestTags
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.MarketsTestTags
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 SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapChooseTokenPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.common_choose_token))
}
val myTokensTitle: KNode = child {
hasText(getResourceString(R.string.exchange_tokens_available_tokens_header))
}
val searchIcon: KNode = child {
hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON)
}
val searchTextField: KNode = child {
hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD)
}
val noTokensFoundText: KNode = child {
hasText(getResourceString(R.string.express_token_list_empty_search))
}
fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child {
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
hasAnyDescendant(withText(tokenTitle))
if (!availableForSwap) {
hasAnyDescendant(
withText(
getResourceString(R.string.tokens_list_unavailable_to_swap_source_header)
)
)
}
useUnmergedTree = true
}
fun marketsTokenWithTitle(title: String): KNode {
return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
hasText(title)
}
}
}
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -29,8 +29,8 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
}
val youSwapBlock: KNode = child {
hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK)
hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_swap)))
hasTestTag(SwapTokenScreenTestTags.SWAP_CARD)
hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title)))
useUnmergedTree = true
}
@ -40,8 +40,8 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
}
val youReceiveBlock: KNode = child {
hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK)
hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_receive)))
hasTestTag(SwapTokenScreenTestTags.RECEIVE_CARD)
hasAnyDescendant(withText(getResourceString(R.string.swapping_to_title)))
useUnmergedTree = true
}

View file

@ -143,7 +143,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
val youSwapBlock: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER)
hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title)))
hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title_v2)))
hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE))
useUnmergedTree = true
}
@ -174,8 +174,16 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT)
}
val selectTokenIcon: KNode = child {
val swapSelectTokenIcon: KNode = child {
hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.SWAP_CARD))
hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON)
useUnmergedTree = true
}
val receiveSelectTokenIcon: KNode = child {
hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.RECEIVE_CARD))
hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON)
useUnmergedTree = true
}
fun swapTokenSymbol(symbol: String): KNode = child {
@ -191,6 +199,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(symbol)
useUnmergedTree = true
}
val chooseTokenButton: KNode = child {
hasText(getResourceString(R.string.common_choose_token))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =

View file

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

View file

@ -16,6 +16,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
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 androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
@ -192,6 +193,20 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasText(getResourceString(R.string.common_buy_currency, feeCurrencySymbol))
useUnmergedTree = true
}
fun expressStatusItem(title: String): KNode = child {
hasTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM)
hasAnyDescendant(
withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TITLE) and withText(title)
)
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_ICON))
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_AMOUNT))
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON))
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON))
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT))
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_CHEVRON_ICON))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =

View file

@ -38,6 +38,18 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
hasText(getResourceString(R.string.settings_forget_wallet))
}
val accountsListContainer: KNode = walletSettingsItem.child {
hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER)
}
val addAccountButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.account_form_create_button))
}
val openArchivedAccountsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.account_archived_accounts))
}
fun accountItem(accountName: String): KNode = walletSettingsItem.child {
hasTestTag(WalletSettingsScreenTestTags.USER_ACCOUNT_ITEM)
hasAnyDescendant(withText(accountName))

View file

@ -1,8 +1,8 @@
package com.tangem.screens
package com.tangem.screens.accounts
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.AccountDetailsScreenTestTags
import com.tangem.core.ui.test.accounts.AccountDetailsScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
@ -11,6 +11,10 @@ import io.github.kakaocup.compose.node.element.KNode
class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AccountDetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.ACCOUNT_DETAILS_CONTAINER)
}
val topAppBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
@ -18,7 +22,16 @@ class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
val manageTokensButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON)
}
val archiveAccountButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.ARCHIVE_ACCOUNT_BUTTON)
}
val editAccountButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.EDIT_ACCOUNT_BUTTON)
}
}
internal fun BaseTestCase.onAccountDetails(function: AccountDetailsPageObject.() -> Unit) =
internal fun BaseTestCase.onAccountDetailsScreen(function: AccountDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,70 @@
package com.tangem.screens.accounts
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.accounts.AccountRowTestTags
import com.tangem.core.ui.test.accounts.ArchivedAccountsScreenTestTags
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 ArchivedAccountsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ArchivedAccountsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(ArchivedAccountsScreenTestTags.ARCHIVED_ACCOUNTS_SCREEN_CONTAINER)
}
val topAppBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.account_archived_accounts))
useUnmergedTree = true
}
/**
* Returns a composite handle for a single archived account row, scoped by account name.
* All sub-elements (icon, title, tokens, networks, restore button) are children of this row.
*/
fun findArchivedAccountItemByName(accountName: String): ArchivedAccountRow {
val container: KNode = child {
hasTestTag(ArchivedAccountsScreenTestTags.ARCHIVED_ACCOUNT_ITEM)
hasAnyDescendant(withText(accountName))
useUnmergedTree = true
}
return ArchivedAccountRow(container)
}
class ArchivedAccountRow(val container: KNode) {
val icon: KNode = container.child {
hasTestTag(AccountRowTestTags.ICON)
useUnmergedTree = true
}
val title: KNode = container.child {
hasTestTag(AccountRowTestTags.TITLE)
useUnmergedTree = true
}
val subtitle: KNode = container.child {
hasTestTag(AccountRowTestTags.SUBTITLE)
useUnmergedTree = true
}
val restoreButton: KNode = container.child {
hasTestTag(ArchivedAccountsScreenTestTags.RESTORE_BUTTON)
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onArchivedAccountsScreen(function: ArchivedAccountsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -8,6 +8,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickAndWaitFor
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.card.ScanFailsRequester
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import com.tangem.scenarios.checkFailedTransactionDialog
@ -17,7 +18,6 @@ import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.ThirdPartyAppPageObject
import com.tangem.screens.onCreateWalletStartScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onFailedTransactionDialog
import com.tangem.screens.onMainScreen
import com.tangem.screens.onScanWarningDialog
@ -28,16 +28,18 @@ import com.tangem.screens.onStoriesScreen
import com.tangem.screens.onTokenDetailsScreen
import com.tangem.screens.onMainScreenTopBar
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class FeedbackTest : BaseTestCase() {
@Inject
lateinit var scanFailsRequester: ScanFailsRequester
@AllureId("894")
@DisplayName("Send feedback: from details")
@Test
@ -69,7 +71,6 @@ class FeedbackTest : BaseTestCase() {
}
}
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("893")
@DisplayName("Send feedback: failed transaction")
@Test
@ -163,9 +164,6 @@ class FeedbackTest : BaseTestCase() {
MockProvider.resetEmulateError()
}
).run {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Set scanning error") {
MockProvider.setEmulateError(TangemSdkError.TagLost())
}
@ -177,9 +175,8 @@ class FeedbackTest : BaseTestCase() {
}
step("Force show 'Scan warning' dialog"){
runOnUiThread {
val requester = store.state.daggerGraphState.scanFailsRequester!!
MainScope().launch {
requester.show(AnalyticsParam.ScreensSources.Main)
scanFailsRequester.show(AnalyticsParam.ScreensSources.Main)
}
}
}

View file

@ -2,10 +2,9 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openOrganizeTokensScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
@ -31,12 +30,8 @@ class OrganizeTokensTest : BaseTestCase() {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Swipe to 'Organize tokens' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
step("Open 'Organize tokens' screen") {
openOrganizeTokensScreen()
}
step("Assert 'Organize tokens' screen is opened") {
onOrganizeTokensScreen {
@ -56,12 +51,8 @@ class OrganizeTokensTest : BaseTestCase() {
step("Assert tokens were grouped on 'Main screen'") {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Swipe to 'Organize tokens' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
step("Open 'Organize tokens' screen") {
openOrganizeTokensScreen()
}
step("Assert 'Organize tokens' screen is opened") {
onOrganizeTokensScreen {
@ -104,12 +95,8 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
step("Open 'Organize tokens' screen") {
openOrganizeTokensScreen()
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {
@ -141,12 +128,8 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
step("Open 'Organize tokens' screen") {
openOrganizeTokensScreen()
}
step("Drag $bitcoinTitle down on 'Organize tokens' screen") {
composeTestRule.waitUntil(timeoutMillis = 100_000) {
@ -191,12 +174,8 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
step("Open 'Organize tokens' screen") {
openOrganizeTokensScreen()
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {

View file

@ -40,8 +40,8 @@ class StakingTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButton().assertIsDisplayed() }
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
@ -98,8 +98,8 @@ class StakingTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButton().assertIsDisplayed() }
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
@ -156,8 +156,8 @@ class StakingTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButton().assertIsDisplayed() }
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)

View file

@ -10,6 +10,7 @@ import com.tangem.screens.onStoriesScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
@ -18,6 +19,7 @@ class TermsOfServiceTest : BaseTestCase() {
@AllureId("3573")
@DisplayName("ToS: success acceptance")
@Test
@Ignore("[REDACTED_JIRA]")
fun validateTermsOfServiceScreenTest() {
setupHooks().run {
val tosUrl = "https://tangem.com/tangem_tos.html"
@ -46,6 +48,7 @@ class TermsOfServiceTest : BaseTestCase() {
@AllureId("3574")
@DisplayName("ToS: accept after app restart")
@Test
@Ignore("[REDACTED_JIRA]")
fun acceptTermsOfServiceAfterAppRestart() {
val packageName = getTargetContext().packageName
val tosUrl = "https://tangem.com/tangem_tos.html"

View file

@ -0,0 +1,253 @@
package com.tangem.tests.accounts
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.REFERRAL_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.*
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.accounts.onArchivedAccountsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onWalletSettingsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class AccountArchivationsTest : BaseTestCase() {
private val userTokensScenario = USER_TOKENS_API_SCENARIO
private val referralScenario = REFERRAL_API_SCENARIO
@Test
@AllureId("5979")
@DisplayName("Accounts: Verify main account archivation button not available")
fun mainAccountArchivationAttemptTest() {
val mainAccountName = "Main account"
setupHooks().run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $mainAccountName") { openAccountDetails(mainAccountName) }
step("Assert archive button is NOT displayed") {
onAccountDetailsScreen { archiveAccountButton.assertDoesNotExist() }
}
}
}
@Test
@AllureId("5974")
@DisplayName("Accounts: archive a non-main account")
fun archiveSuccessfullyAccountTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivable"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Assert 'Archive' button is displayed") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
step("Click on 'Archive' button") {
onAccountDetailsScreen { archiveAccountButton.clickWithAssertion() }
}
step("Verify 'Archive confirmation' dialog appeared with all elements") {
assertArchiveConfirmationDialog()
}
step("Click 'Archive' in confirmation menu") {
onDialog { archiveButton.clickWithAssertion() }
}
step("Verify app returned to 'Wallet settings' screen") {
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
}
step("Verify archived account '$accountToArchiveName' is no longer listed") {
onWalletSettingsScreen {
accountItem(accountToArchiveName).assertDoesNotExist()
}
}
}
}
@Test
@AllureId("6844")
@DisplayName("Accounts: account archivation error on UI")
fun archiveAccountErrorTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivable"
val userAccountsErrorState = "AccountsPutError"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Forcing network error scenario") {
setWireMockScenarioState(userTokensScenario, userAccountsErrorState)
}
step("Attempt to archive the account") { archiveAccount() }
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.common_something_went_wrong),
expectedMessage = getResourceString(R.string.account_generic_error_dialog_message),
)
}
}
}
@Test
@AllureId("5981")
@DisplayName("Accounts: archive account with referral program error")
fun archiveAccountReferralErrorTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivableAndReferral"
val referralActiveState = "Participating"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
setWireMockScenarioState(referralScenario, referralActiveState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
resetWireMockScenarioState(referralScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Attempt to archive the account") { archiveAccount() }
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.account_could_not_archive_referral_program_title),
expectedMessage = getResourceString(R.string.account_could_not_archive_referral_program_message),
)
}
step("Dismiss error dialog") { dismissErrorDialog() }
step("Assert 'Archive' button is still visible after error") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
}
}
@Test
@AllureId("5976")
@DisplayName("Accounts: restore an archived account")
fun restoreArchivedAccountTest() {
val archivedAccountName = "Account 3"
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
val userAccountsAfterArchivationState = "ReadyToRestore"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() }
step("Verify archived wallet account with name '$archivedAccountName' is present") {
assertArchivedAccountIsDisplayed(archivedAccountName)
}
step("Switch WireMock to '$userAccountsAfterArchivationState' users scenario state") {
setWireMockScenarioState(userTokensScenario, userAccountsAfterArchivationState)
}
step("Restore account with name '$archivedAccountName'") {
restoreArchivedAccount(archivedAccountName)
}
step("Assert 'Wallet settings' screen is displayed") {
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
}
step("Assert restored account '$archivedAccountName' appears in 'Active accounts' list") {
onWalletSettingsScreen { accountItem(archivedAccountName).assertIsDisplayed() }
}
}
}
@Test
@AllureId("7962")
@DisplayName("Accounts: restore archived account error")
fun restoreArchivedAccountErrorTest() {
val archivedAccountName = "Account 3"
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
val userAccountsRestorationErrorState = "AccountsPutError"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() }
step("Verify archived wallet account with name '$archivedAccountName' is present") {
assertArchivedAccountIsDisplayed(archivedAccountName)
}
step("Switch WireMock to '$userAccountsRestorationErrorState' user" +
"accounts scenario state to simulate restoration failure") {
setWireMockScenarioState(userTokensScenario, userAccountsRestorationErrorState)
}
step("Attempt restore account with name '$archivedAccountName'") {
restoreArchivedAccount(archivedAccountName)
}
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.common_something_went_wrong),
expectedMessage = getResourceString(R.string.account_generic_error_dialog_message),
)
}
step("Dismiss error dialog") { dismissErrorDialog() }
step("Assert archived account '$archivedAccountName' is still in archived list") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(archivedAccountName)
.container.assertIsDisplayed()
}
}
}
}
}

View file

@ -21,7 +21,6 @@ import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
@ -462,7 +461,6 @@ class MainScreenActionButtonsTest : BaseTestCase() {
}
}
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("4396")
@DisplayName("Action buttons (main screen): click on buttons without data")
@Test

View file

@ -73,7 +73,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Open 'Markets screen'") {
swipeMarketsBlock(SwipeDirection.UP)
onMainScreen { searchThroughMarketPlaceholder.performClick() }
waitForIdle()
}
step("Click on $tokenTitle token") {

View file

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

View file

@ -9,6 +9,7 @@ import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.accounts.onAccountDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -93,7 +94,7 @@ class HideTokenTest : BaseTestCase() {
onWalletSettingsScreen { accountItem(accountName).performClick() }
}
step("Click on 'Manage tokens' button") {
onAccountDetails { manageTokensButton.performClick() }
onAccountDetailsScreen { manageTokensButton.performClick() }
}
step("Click on token: '$tokenTitle'") {
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
@ -118,7 +119,7 @@ class HideTokenTest : BaseTestCase() {
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetails { topAppBarBackButton.performClick() }
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
@ -162,7 +163,7 @@ class HideTokenTest : BaseTestCase() {
onWalletSettingsScreen { accountItem(accountName).performClick() }
}
step("Click on 'Manage tokens' button") {
onAccountDetails { manageTokensButton.performClick() }
onAccountDetailsScreen { manageTokensButton.performClick() }
}
step("Click on token: '$tokenTitle'") {
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
@ -187,7 +188,7 @@ class HideTokenTest : BaseTestCase() {
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetails { topAppBarBackButton.performClick() }
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
@ -308,5 +309,4 @@ class HideTokenTest : BaseTestCase() {
}
}
}
}

View file

@ -28,8 +28,8 @@ class MainScreenTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButton().assertIsDisplayed() }
}
}
}
@ -56,8 +56,8 @@ class MainScreenTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is not displayed") {
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
step("Assert 'Add & Manage' button is not displayed") {
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
}
}
}
@ -81,8 +81,8 @@ class MainScreenTest : BaseTestCase() {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Organize tokens' button is not displayed") {
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
step("Assert 'Add & Manage' button is not displayed") {
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
}
}
}
@ -106,8 +106,8 @@ class MainScreenTest : BaseTestCase() {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Organize tokens' button is not displayed") {
onMainScreen { organizeTokensButtonNode.assertIsDisplayed()}
step("Assert 'Add & Manage' button is displayed") {
onMainScreen { addAndManageButtonNode.assertIsDisplayed()}
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.scenarios.assertMarketsExchangesScreen
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openMarketsExchangesScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.screens.onMarketsExchangesScreen
import com.tangem.screens.onMarketsScreen
import dagger.hilt.android.testing.HiltAndroidTest
@ -52,7 +53,7 @@ class MarketsExchangesTest : BaseTestCase() {
synchronizeAddresses()
}
step("Open 'Markets' screen") {
swipeMarketsBlock(SwipeDirection.UP)
onMainScreen { searchThroughMarketPlaceholder.performClick() }
waitForIdle()
}
step("Click on '$tokenName' token") {

View file

@ -2,15 +2,20 @@ package com.tangem.tests.send.sendViaSwap
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.common.extensions.extractText
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.POLYGON_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.extractText
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
import com.tangem.scenarios.openSendScreen
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
@ -30,6 +35,7 @@ class SendViaSwapTest : BaseTestCase() {
val bitcoinBalanceScenarioState = "Balance"
val assetsScenarioName = "express_api_assets"
val assetsScenarioState = "BitcoinExchangeEnabled"
val userTokensScenarioState = "Wallet2"
val warningTitle = getResourceString(R.string.express_swap_not_supported_title, stellar)
val warningMessage = getResourceString(R.string.express_swap_not_supported_text)
@ -37,6 +43,7 @@ class SendViaSwapTest : BaseTestCase() {
additionalAfterSection = {
resetWireMockScenarioState(bitcoinBalanceScenarioName)
resetWireMockScenarioState(assetsScenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
@ -46,9 +53,11 @@ class SendViaSwapTest : BaseTestCase() {
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
}
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensScenarioState)
}
step("Open 'Send' screen") {
openSendScreen(tokenName)
openSendScreen(tokenName, mockContent = Wallet2WithDerivationsMockContent)
}
step("Click on 'Swap to another token button'") {
onSendScreen { swapToAnotherTokenButton.performClick() }
@ -89,11 +98,13 @@ class SendViaSwapTest : BaseTestCase() {
val bitcoinBalanceScenarioState = "Balance"
val assetsScenarioName = "express_api_assets"
val assetsScenarioState = "BitcoinExchangeEnabled"
val userTokensScenarioState = "Wallet2"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(bitcoinBalanceScenarioName)
resetWireMockScenarioState(assetsScenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
@ -103,9 +114,12 @@ class SendViaSwapTest : BaseTestCase() {
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
}
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensScenarioState)
}
step("Open 'Send' screen") {
openSendScreen(tokenName)
openSendScreen(tokenName, mockContent = Wallet2WithDerivationsMockContent)
}
step("Type '$firstInputAmount' in text field") {
onSendScreen { amountInputTextField.performTextInput(firstInputAmount) }
@ -174,10 +188,13 @@ class SendViaSwapTest : BaseTestCase() {
step("Assert provider name is '$regularProviderName'") {
onSendConfirmScreen { providerName.assertTextContains(regularProviderName) }
}
step("Click on fee selector icon") {
onSendConfirmScreen { feeSelectorIcon.performClick() }
step("Open fee selector bottom sheet via click on fee selector icon") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSendConfirmScreen { feeSelectorIcon.performClick() }
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
}
}
step("Click on '$fastSelectorItem' selector item is displayed") {
step("Click on '$fastSelectorItem' selector item") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).performClick() }
}
step("Capture fast fee value") {
@ -228,6 +245,7 @@ class SendViaSwapTest : BaseTestCase() {
val bitcoinBalanceScenarioState = "Balance"
val assetsScenarioName = "express_api_assets"
val assetsScenarioState = "BitcoinExchangeEnabled"
val userTokensScenarioState = "Wallet2"
val dialogTitle = getResourceString(R.string.send_with_swap_change_token_alert_title)
val dialogMessage = getResourceString(R.string.send_with_swap_change_token_alert_message)
val addressHint = getResourceString(R.string.send_enter_address_field_ens)
@ -236,6 +254,7 @@ class SendViaSwapTest : BaseTestCase() {
additionalAfterSection = {
resetWireMockScenarioState(bitcoinBalanceScenarioName)
resetWireMockScenarioState(assetsScenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
@ -245,9 +264,12 @@ class SendViaSwapTest : BaseTestCase() {
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
}
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensScenarioState)
}
step("Open 'Send' screen") {
openSendScreen(tokenName)
openSendScreen(tokenName, mockContent = Wallet2WithDerivationsMockContent)
}
step("Type '$inputAmount' in text field") {
onSendScreen { amountInputTextField.performTextInput(inputAmount) }
@ -369,4 +391,278 @@ class SendViaSwapTest : BaseTestCase() {
}
}
}
@AllureId("3967")
@DisplayName("Send via Swap: full successful send via swap flow")
@Test
fun sendViaSwapSuccessfulFlowTest() {
val tokenName = "Bitcoin"
val swapTokenName = "Ethereum"
val main = "MAIN"
val inputAmount = "0.001"
val providerName = "SimpleSwap"
val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
val bitcoinBalanceScenarioName = "bitcoin_utxo"
val bitcoinBalanceScenarioState = "BalanceHotWalletSvS"
val assetsScenarioName = "express_api_assets"
val assetsScenarioState = "BitcoinExchangeEnabled"
val hotWalletScenarioState = "HotWalletSvS"
val providersScenarioName = "networks_providers"
val providersScenarioState = "HotWalletSvS"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(bitcoinBalanceScenarioName)
resetWireMockScenarioState(assetsScenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(providersScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$bitcoinBalanceScenarioName' to state: '$bitcoinBalanceScenarioState'") {
setWireMockScenarioState(scenarioName = bitcoinBalanceScenarioName, state = bitcoinBalanceScenarioState)
}
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
}
step("Set WireMock scenario: '$providersScenarioName' to state: '$providersScenarioState'") {
setWireMockScenarioState(scenarioName = providersScenarioName, state = providersScenarioState)
}
step("Open 'Main Screen' with existing hot wallet") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Select token to Send via Swap") {
selectTokenToSendViaSwap(swapTokenName = swapTokenName, networkName = swapTokenName, networkType = main)
}
step("Type '$inputAmount' in text field") {
onSendScreen { amountInputTextField.performTextInput(inputAmount) }
}
step("Click on 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSendScreen {
nextButton.assertIsEnabled()
nextButton.performClick()
}
}
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
}
step("Open 'Send confirm' screen via 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendConfirmScreenViaNextButton()
}
}
step("Assert 'Best rate' badge is displayed") {
onSendConfirmScreen { bestRateBadge.assertIsDisplayed() }
}
step("Open 'Send via swap success' screen") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendSuccessScreenViaLongClickOnSendButton()
}
}
step("Check 'Send via swap' screen") {
checkSendViaSwapSuccessScreen()
}
step("Click on 'Explore' button") {
onSendSuccessScreen { exploreButton.performClick() }
}
step("Assert Chrome Browser is opened") {
ThirdPartyAppPageObject { assertChromeIsOpened() }
}
step("Press 'Back' button to close 'Chrome' browser") {
device.uiDevice.pressBack()
}
step("Click on 'Close' button") {
onSendSuccessScreen { closeButton.performClick() }
}
step("Assert 'Express status' item is displayed with title: '$expressStatusItemTitle'") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
}
}
}
}
@AllureId("4017")
@DisplayName("Send via Swap: send same token in different network")
@Test
fun sendSameTokenInDifferentNetworkTest() {
val tokenName = "Tether"
val swapTokenName = "Tether"
val networkName = "Polygon"
val inputAmount = "0.001"
val ethCallScenarioName = "eth_call_api"
val ethCallScenarioState = "Started"
val hotWalletScenarioState = "USDTHotWalletSvS"
val ethNetworkBalanceScenarioName = "eth_network_balance"
val ethNetworkBalanceScenarioState = "Started"
val providerName = "Changelly"
val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(ethCallScenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(ethNetworkBalanceScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$ethCallScenarioName' to state: '$ethCallScenarioState'") {
setWireMockScenarioState(scenarioName = ethCallScenarioName, state = ethCallScenarioState)
}
step("Set WireMock scenario: '$ethNetworkBalanceScenarioName' to state: '$ethNetworkBalanceScenarioState'") {
setWireMockScenarioState(scenarioName = ethNetworkBalanceScenarioName, state = ethNetworkBalanceScenarioState)
}
step("Open 'Main Screen' with existing hot wallet") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Select token to Send via Swap") {
selectTokenToSendViaSwap(swapTokenName = swapTokenName, networkName = networkName)
}
step("Type '$inputAmount' in text field") {
onSendScreen { amountInputTextField.performTextInput(inputAmount) }
}
step("Click on 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSendScreen {
nextButton.assertIsEnabled()
nextButton.performClick()
}
}
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(POLYGON_RECIPIENT_ADDRESS) }
}
step("Open 'Send confirm' screen via 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendConfirmScreenViaNextButton()
}
}
step("Assert 'Best rate' badge is displayed") {
onSendConfirmScreen { bestRateBadge.assertIsDisplayed() }
}
step("Open 'Send via swap success' screen") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendSuccessScreenViaLongClickOnSendButton()
}
}
step("Check 'Send via swap' screen") {
checkSendViaSwapSuccessScreen()
}
step("Click on 'Close' button") {
onSendSuccessScreen { closeButton.performClick() }
}
step("Assert 'Express status' item is displayed with title: '$expressStatusItemTitle'") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
}
}
}
}
@AllureId("4545")
@DisplayName("Send via Swap: token with tag/memo send via swap flow")
@Test
fun sendViaSwapTokenWithTagTest() {
val tokenName = "XRP Ledger"
val swapTokenName = "Ethereum"
val main = "MAIN"
val inputAmount = "0.001"
val providerName = "Changelly"
val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
val hotWalletScenarioState = "XRPHotWalletSvS"
val xrpExchangeQuoteScenarioName = "xrp_exchange_quote"
val xrpExchangeDataScenarioName = "xrp_exchange_data"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(xrpExchangeQuoteScenarioName)
resetWireMockScenarioState(xrpExchangeDataScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$xrpExchangeQuoteScenarioName' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = xrpExchangeQuoteScenarioName, state = hotWalletScenarioState)
}
step("Set WireMock scenario: '$xrpExchangeDataScenarioName' to state: '$hotWalletScenarioState'") {
setWireMockScenarioState(scenarioName = xrpExchangeDataScenarioName, state = hotWalletScenarioState)
}
step("Open 'Main Screen' with existing hot wallet") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Select token to Send via Swap") {
selectTokenToSendViaSwap(swapTokenName = swapTokenName, networkName = swapTokenName, networkType = main)
}
step("Type '$inputAmount' in text field") {
onSendScreen { amountInputTextField.performTextInput(inputAmount) }
}
step("Click on 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSendScreen {
nextButton.assertIsEnabled()
nextButton.performClick()
}
}
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
}
step("Open 'Send confirm' screen via 'Next' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendConfirmScreenViaNextButton()
}
}
step("Open 'Send via swap success' screen") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
openSendSuccessScreenViaLongClickOnSendButton()
}
}
step("Check 'Send via swap' screen") {
checkSendViaSwapSuccessScreen()
}
step("Click on 'Close' button") {
onSendSuccessScreen { closeButton.performClick() }
}
step("Assert 'Express status' item is displayed with title: '$expressStatusItemTitle'") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
}
}
}
}
}

View file

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

View file

@ -0,0 +1,160 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSendConfirmScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendSelectNetworkFeeBottomSheet
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class CommonWarningsTest : BaseTestCase() {
@AllureId("4293")
@DisplayName("Warnings: check warning, when custom fee lower than 'Slow'")
@Test
fun warningDisplayedWhenCustomFeeLowerThanSlowTest() {
val tokenName = "Ethereum"
val sendAmount = "0.01"
val feeUpTo = getResourceString(R.string.send_max_fee)
val customFee = "0.000000001"
val warningTitle = getResourceString(R.string.send_notification_transaction_delay_title)
val warningMessage = getResourceString(R.string.send_notification_transaction_delay_text)
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Confirm' screen with token: $tokenName") {
openSendConfirmScreen(tokenName, sendAmount, ETHEREUM_RECIPIENT_ADDRESS)
}
step("Click on fee selector icon") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSendConfirmScreen { feeSelectorIcon.performClick() }
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
}
}
step("Click on 'Custom' selector item") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
}
step("Type '$customFee' into input text field") {
onSendSelectNetworkFeeBottomSheet {
inputTextFieldValue(title = feeUpTo).performClick()
inputTextFieldValue(title = feeUpTo).performTextReplacement(customFee)
}
}
step("Click on 'Done' button") {
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
}
step("Assert 'Transaction delays are possible' warning is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = true,
sendButtonIsDisabled = false
)
}
}
}
@AllureId("4221")
@DisplayName("Warnings: check warning, when amount exceeds total balance")
@Test
fun warningDisplayedWhenAmountExceedsTotalBalanceTest() {
val tokenName = "Ethereum"
val sendAmount = "0.9999"
val network = "ETH 0.00032"
val amount = "\$0.81"
val warningTitle = getResourceString(R.string.send_network_fee_warning_title)
val warningMessage = getResourceString(R.string.common_network_fee_warning_content, network, amount)
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
synchronizeAddresses()
}
}
step("Open 'Send Confirm' screen with token: $tokenName") {
openSendConfirmScreen(tokenName, sendAmount, ETHEREUM_RECIPIENT_ADDRESS)
}
step("Assert 'Network fee coverage' warning is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = true,
sendButtonIsDisabled = false
)
}
}
}
@AllureId("4294")
@DisplayName("Warnings: check warning, when custom fee is high")
@Test
fun warningDisplayedWhenCustomFeeIsHighTest() {
val tokenName = "Ethereum"
val sendAmount = "0.01"
val feeUpTo = getResourceString(R.string.send_max_fee)
val customFee = "0.004"
val timesHigher = "7"
val warningTitle = getResourceString(R.string.send_notification_fee_too_high_title)
val warningMessage = getResourceString(R.string.send_notification_fee_too_high_text, timesHigher)
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Confirm' screen with token: $tokenName") {
openSendConfirmScreen(tokenName, sendAmount, ETHEREUM_RECIPIENT_ADDRESS)
}
step("Click on fee selector icon") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSendConfirmScreen { feeSelectorIcon.performClick() }
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
}
}
step("Click on 'Custom' selector item") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
}
step("Type '$customFee' into input text field") {
onSendSelectNetworkFeeBottomSheet {
inputTextFieldValue(title = feeUpTo).performClick()
inputTextFieldValue(title = feeUpTo).performTextReplacement(customFee)
}
}
step("Click on 'Done' button") {
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
}
step("Assert 'Custom fee is high' warning is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = true,
sendButtonIsDisabled = false
)
}
}
}
}

View file

@ -238,20 +238,17 @@ class SearchAndSwapTest : BaseTestCase() {
onSwapTokenScreen { replaceTokensButton.performClick() }
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
onSwapTokenScreen { swapSelectTokenIcon.performClick() }
}
step("Click on 'Search' icon") {
onSwapChooseTokenScreen { searchIcon.performClick() }
}
step("Click on 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performClick() }
onSwapSelectTokenScreen { searchBarIcon.performClick() }
}
step("Type '$swapTokenSymbol' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextInputInChunks(swapTokenSymbol) }
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(swapTokenSymbol) }
}
step("Click on token with name: '$swapTokenName'") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSwapChooseTokenScreen { marketsTokenWithTitle(swapTokenName).performClick() }
onSwapSelectTokenScreen { marketsTokenWithName(swapTokenName).performClick() }
}
}
step("Click on 'Add' button") {

View file

@ -4,7 +4,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.*
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.datasource.api.common.config.ApiConfig
@ -30,7 +30,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() {
@Test
fun checkAvailableToSwapTokensListTest() {
val tokenTitle = "Polygon"
val inputAmount = "100"
val ethereum = "Ethereum"
val polExMatic = "POL (ex-MATIC)"
val bitcoin = "Bitcoin"
@ -66,30 +65,23 @@ class SwapChooseTokenScreenTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
onSwapTokenScreen { swapSelectTokenIcon.performClick() }
}
step("Assert '$ethereum' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() }
onSwapSelectTokenScreen { tokenWithName(ethereum).assertIsDisplayed() }
}
step("Assert '$polExMatic' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
onSwapSelectTokenScreen { tokenWithName(polExMatic).assertIsDisplayed() }
}
step("Assert '$bitcoin' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() }
onSwapSelectTokenScreen { tokenWithName(bitcoin).assertIsNotDisplayed() }
}
step("Assert '$jesusCoin' is displayed and unavailable for swap") {
onSwapChooseTokenScreen { tokenWithTitle(tokenTitle = jesusCoin).assertIsDisplayed() }
onSwapSelectTokenScreen { tokenWithName(jesusCoin).assertIsDisplayed() }
}
step("Assert custom token without backend id '$salam' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() }
onSwapSelectTokenScreen { tokenWithName(salam).assertIsNotDisplayed() }
}
}
}
@ -102,7 +94,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() {
@Test
fun checkSearchOnSwapChooseTokenScreenTest() {
val tokenTitle = "Polygon"
val inputAmount = "100"
val ethereum = "Ethereum"
val polExMatic = "POL (ex-MATIC)"
val polExMaticSymbol = "POL"
@ -129,45 +120,35 @@ class SwapChooseTokenScreenTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
step("Click on 'Search' icon") {
onSwapChooseTokenScreen { searchIcon.performClick() }
}
step("Click on 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performClick() }
onSwapSelectTokenScreen { searchBarIcon.performClick() }
}
step("Type invalid search text: '$invalidSearchText' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) }
onSwapSelectTokenScreen { searchBarBlock.performTextReplacement(invalidSearchText) }
}
step("Assert '$ethereum' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
onSwapSelectTokenScreen { tokenWithName(ethereum).assertIsNotDisplayed() }
}
step("Assert '$polExMatic' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() }
onSwapSelectTokenScreen { tokenWithName(polExMatic).assertIsNotDisplayed() }
}
step("Press 'Delete' button") {
device.uiDevice.pressDelete()
}
step("Type valid search text: '$validSearchText' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) }
onSwapSelectTokenScreen { searchBarBlock.performTextReplacement(validSearchText) }
}
step("Assert '$ethereum' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
onSwapSelectTokenScreen { tokenWithName(ethereum).assertIsNotDisplayed() }
}
step("Assert '$polExMatic' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
onSwapSelectTokenScreen { tokenWithName(polExMatic).assertIsDisplayed() }
}
step("Select new receive token: $polExMatic") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() }
onSwapSelectTokenScreen { tokenWithName(polExMatic).performClick() }
}
step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).assertIsDisplayed() }

View file

@ -0,0 +1,38 @@
package com.tangem.tests.swap
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
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 SwapMainScreenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD),
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("574")
@DisplayName("Swap: 'Swap' button is not displayed for single currency card")
@Test
fun singleTokenNoteCardScanTest() {
val cardType: ProductType = ProductType.Note
setupHooks().run {
step("Open 'Main Screen' on '${cardType.name}' card") {
openMainScreen(cardType)
}
step("Assert 'Swap' button is not displayed") {
onMainScreen { swapButton.assertIsNotDisplayed() }
}
}
}
}

View file

@ -20,7 +20,6 @@ class SwapSelectTokenScreenTest : BaseTestCase() {
@DisplayName("Open 'Swap select token' screen from 'Main' screen")
@Test
fun openSwapSelectTokenScreenFromMainScreenTest() {
val swapTokenName = "Ethereum"
val receiveTokenName = "Polygon"
setupHooks().run {
@ -37,14 +36,11 @@ class SwapSelectTokenScreenTest : BaseTestCase() {
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap select token' screen title is displayed") {
onSwapSelectTokenScreen { title.assertIsDisplayed() }
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
step("Assert 'You swap' title is displayed") {
onSwapSelectTokenScreen { youSwapTitle.assertIsDisplayed() }
}
step("Assert 'You swap' block is displayed") {
onSwapSelectTokenScreen { youSwapBlock.assertIsDisplayed() }
step("Assert 'You receive' title is displayed") {
onSwapSelectTokenScreen { youReceiveTitle.assertIsDisplayed() }
}
step("Assert search icon is displayed") {
onSwapSelectTokenScreen { searchBarIcon.assertIsDisplayed() }
@ -52,15 +48,6 @@ class SwapSelectTokenScreenTest : BaseTestCase() {
step("Assert search placeholder is displayed") {
onSwapSelectTokenScreen { searchBarPlaceholderText.assertIsDisplayed() }
}
step("Click on token with name '$swapTokenName'") {
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
}
step("Assert 'You receive' title is displayed") {
onSwapSelectTokenScreen { youReceiveTitle.assertIsDisplayed() }
}
step("Assert 'You receive' block is displayed") {
onSwapSelectTokenScreen { youReceiveBlock.assertIsDisplayed() }
}
step("Click on token with name '$receiveTokenName'") {
onSwapSelectTokenScreen { tokenWithName(receiveTokenName).performClick() }
}

View file

@ -117,7 +117,7 @@ class SwapStoriesTest : BaseTestCase() {
@DisplayName("Check unavailable swap stories on 'Main' screen")
@Test
fun checkUnavailableSwapStoriesOnMainScreen() {
val scenarioName = "stories_first_time_swap"
val scenarioName = "stories_first_time_swap_v2"
val scenarioErrorState = "Error"
val packageName = getTargetContext().packageName
@ -168,7 +168,7 @@ class SwapStoriesTest : BaseTestCase() {
@DisplayName("Check unavailable swap stories on 'Token details' screen")
@Test
fun checkUnavailableSwapStoriesOnTokenDetailsScreen() {
val scenarioName = "stories_first_time_swap"
val scenarioName = "stories_first_time_swap_v2"
val scenarioErrorState = "Error"
val packageName = getTargetContext().packageName
val tokenName = "Ethereum"
@ -224,7 +224,7 @@ class SwapStoriesTest : BaseTestCase() {
@DisplayName("Check unavailable swap stories on 'Markets' token details screen")
@Test
fun checkUnavailableSwapStoriesOnMarketsTokenDetailsScreen() {
val scenarioName = "stories_first_time_swap"
val scenarioName = "stories_first_time_swap_v2"
val scenarioErrorState = "Error"
val packageName = getTargetContext().packageName
val tokenName = "Ethereum"

View file

@ -32,6 +32,7 @@ class SwapTokenScreenTest : BaseTestCase() {
fun networkFeeTest() {
val inputAmount = "400"
val tokenTitle = "Polygon"
val receiveTokenName = "Ethereum"
setupHooks().run {
@ -67,12 +68,8 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
@ -84,6 +81,13 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Assert 'Providers' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
@ -117,6 +121,7 @@ class SwapTokenScreenTest : BaseTestCase() {
@Test
fun networkErrorSwapTest() {
val tokenTitle = "Polygon"
val receiveTokenName = "Ethereum"
setupHooks(
additionalAfterSection = {
@ -150,6 +155,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Assert error notification title is displayed") {
onSwapTokenScreen {
waitForIdle()
@ -175,9 +183,10 @@ class SwapTokenScreenTest : BaseTestCase() {
@Test
fun changeNetworkFeeTest() {
val inputAmount = "400"
val tokenTitle = "Polygon"
val receiveTokenName = "Ethereum"
setupHooks().run {
val tokenTitle = "Polygon"
step("Open 'Main Screen'") {
openMainScreen()
@ -207,12 +216,8 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
@ -224,6 +229,13 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
@ -270,11 +282,10 @@ class SwapTokenScreenTest : BaseTestCase() {
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("2828")
@DisplayName("Swap: network fee")
@DisplayName("Swap: go to token swap")
@Test
fun goToTokenSwapTest() {
val swapTokenSymbol = "POL"
val receiveTokenSymbol = "ETH"
val tokenTitle = "Polygon"
setupHooks().run {
@ -314,8 +325,8 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Assert token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
step("Assert 'Choose token' button is displayed") {
onSwapTokenScreen { chooseTokenButton.assertIsDisplayed() }
}
}
}
@ -329,6 +340,7 @@ class SwapTokenScreenTest : BaseTestCase() {
fun checkSwapUiTest() {
val swapTokenSymbol = "POL"
val receiveTokenSymbol = "ETH"
val receiveTokenName = "Ethereum"
val newReceiveToken = "POL (ex-MATIC)"
val tokenTitle = "Polygon"
val inputAmount = "1"
@ -356,14 +368,17 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
step("Click on receive 'Select token' icon") {
onSwapTokenScreen { receiveSelectTokenIcon.performClick() }
}
step("Select new receive token: $newReceiveToken") {
onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() }
onSwapSelectTokenScreen { tokenWithName(newReceiveToken).performClick() }
}
step("Assert new receive token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
@ -392,7 +407,11 @@ class SwapTokenScreenTest : BaseTestCase() {
onSwapTokenScreen { swapFiatAmount.assertIsDisplayed() }
}
step("Assert receive token fiat amount is displayed") {
onSwapTokenScreen { receiveFiatAmount.assertIsDisplayed() }
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
receiveFiatAmount.assertIsDisplayed()
}
}
}
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
@ -416,6 +435,7 @@ class SwapTokenScreenTest : BaseTestCase() {
fun checkSwapTokensSwitchTest() {
val swapTokenSymbol = "POL"
val receiveTokenSymbol = "ETH"
val receiveTokenName = "Ethereum"
val tokenTitle = "Polygon"
setupHooks().run {
@ -438,12 +458,19 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
step("Click on 'Swap tokens on screen' button") {
onSwapTokenScreen { replaceTokensButton.performClick() }
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSwapTokenScreen {
replaceTokensButton.assertIsEnabled()
replaceTokensButton.performClick()
}
}
}
step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
@ -514,6 +541,7 @@ class SwapTokenScreenTest : BaseTestCase() {
@Test
fun enableToCoverMarketAndFastFeeTest() {
val tokenName = "Ethereum"
val receiveTokenName = "Polygon"
val inputAmount = "0.99"
val market = "Market"
val fast = "Fast"
@ -537,6 +565,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -562,6 +593,7 @@ class SwapTokenScreenTest : BaseTestCase() {
@Test
fun unableToCoverMarketAndFastFeeTest() {
val tokenName = "POL (ex-MATIC)"
val receiveTokenName = "Ethereum"
val inputAmount = "0.0001"
val marketFeeType = "Market"
val fastFeeType = "Fast"
@ -587,6 +619,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
@ -596,6 +631,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -633,6 +671,7 @@ class SwapTokenScreenTest : BaseTestCase() {
@Test
fun unableToCoverFastFeeTest() {
val tokenName = "POL (ex-MATIC)"
val receiveTokenName = "Ethereum"
val inputAmount = "3000"
val fastFeeType = "Fast"
val fastFeeAmount = "$2,"
@ -660,6 +699,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
@ -669,6 +711,9 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {

View file

@ -15,6 +15,7 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.SwapEntryPoint
import com.tangem.scenarios.chackUnableToCoverFeeNotification
import com.tangem.scenarios.checkSwapWarning
import com.tangem.scenarios.chooseReceiveToken
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSwapScreen
import com.tangem.scenarios.synchronizeAddresses
@ -36,6 +37,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun checkSwapInsufficientFundsWarningTest() {
val tokenTitle = "Polygon"
val receiveTokenTitle = "Ethereum"
val inputAmount = "1000"
setupHooks().run {
@ -58,6 +60,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenTitle)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -120,6 +125,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(networkName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -143,6 +151,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun checkHighPriceImpactWarningCEXTest() {
val tokenTitle = "USDC"
val receiveTokenName = "Solana"
val inputAmount = "100"
val currencySymbol = "SOL"
val slippagePercent = "5%"
@ -195,6 +204,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -236,6 +248,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun checkHighPriceImpactWarningDEXTest() {
val tokenTitle = "Polygon"
val receiveTokenName = "Ethereum"
val inputAmount = "1000"
val slippagePercent = "3.5%"
val dialogTitle = getResourceString(R.string.swapping_alert_title)
@ -280,6 +293,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -323,6 +339,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun solanaRemainingBalanceEqualToZeroWarningTest() {
val tokenTitle = "Solana"
val receiveTokenName = "USDC"
val inputAmount = "0.00168933"
val tokensScenarioState = "SolanaUSDC"
val rentAmount = "SOL 0.00089088"
@ -364,6 +381,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -388,6 +408,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun solanaRemainingBalanceEqualToRentAmountTest() {
val tokenTitle = "Solana"
val receiveTokenName = "USDC"
val inputAmount = "0.001689338"
val tokensScenarioState = "SolanaUSDC"
val rentAmount = "SOL 0.00089088"
@ -429,6 +450,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -454,6 +478,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun solanaRemainingBalanceMoreThanRentAmountTest() {
val tokenTitle = "Solana"
val receiveTokenName = "USDC"
val inputAmount = "0.0000941"
val tokensScenarioState = "SolanaUSDC"
val rentAmount = "SOL 0.00089088"
@ -495,6 +520,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
@ -519,6 +547,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
@Test
fun solanaRemainingBalanceLessThanRentAmountTest() {
val tokenTitle = "Solana"
val receiveTokenName = "USDC"
val inputAmount = "0.0016941"
val tokensScenarioState = "SolanaUSDC"
val rentAmount = "SOL 0.00089088"
@ -560,6 +589,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Choose receive token") {
chooseReceiveToken(receiveTokenName)
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {

View file

@ -232,6 +232,17 @@
android:scheme="tangem" />
</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="token_exchanges"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -285,6 +296,36 @@
android:host="onboard-visa"
android:scheme="tangem" />
</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="news"
android:scheme="tangem" />
</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="earn"
android:scheme="tangem" />
</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="yield"
android:scheme="tangem" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

View file

@ -1,157 +1,52 @@
package com.tangem.tap
import androidx.hilt.work.HiltWorkerFactory
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.common.log.TangemLoggingInitializer
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@EntryPoint
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
interface ApplicationEntryPoint {
fun getAppStateHolder(): AppStateHolder
fun getIssuersConfigStorage(): IssuersConfigStorage
fun getEnvironmentConfig(): EnvironmentConfig
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
fun getWalletManagersFacade(): WalletManagersFacade
fun getAppThemeModeRepository(): AppThemeModeRepository
fun getBalanceHidingRepository(): BalanceHidingRepository
fun getAppPreferencesStore(): AppPreferencesStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase
fun getWalletsRepository(): WalletsRepository
fun getOneTimeEventFilter(): OneTimeEventFilter
fun getWasTwinsOnboardingShownUseCase(): WasTwinsOnboardingShownUseCase
fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase
fun getCardRepository(): CardRepository
fun getTangemSdkLogger(): TangemSdkLogger
fun getSettingsRepository(): SettingsRepository
fun getBlockchainSDKFactory(): BlockchainSDKFactory
fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getWalletMetaInfoUseCase(): GetWalletMetaInfoUseCase
fun getUrlOpener(): UrlOpener
fun getShareManager(): ShareManager
fun getAppRouter(): AppRouter
fun getTangemAppLogger(): TangemAppLoggerInitializer
fun getTransactionSignerFactory(): TransactionSignerFactory
fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles
fun getOnboardingRepository(): OnboardingRepository
fun getExcludedBlockchains(): ExcludedBlockchains
fun getAppLogsStore(): AppLogsStore
fun getClipboardManager(): ClipboardManager
fun getSettingsManager(): SettingsManager
fun getTangemLoggingInitializer(): TangemLoggingInitializer
fun getBlockchainExceptionHandler(): BlockchainExceptionHandler
@GlobalUiMessageSender
fun getUiMessageSender(): UiMessageSender
fun getWorkerFactory(): HiltWorkerFactory
fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory
fun getApiConfigsManager(): ApiConfigsManager
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk
fun getWcInitializeUseCase(): WcInitializeUseCase
fun getTrackingContextProxy(): TrackingContextProxy
fun getABTestsManager(): ABTestsManager
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles
fun getScanFailsRequester(): ScanFailsRequester
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
}

View file

@ -7,12 +7,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -30,6 +30,7 @@ internal class LockUserWalletsTimer(
private val coroutineScope: CoroutineScope,
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
private val passwordRequester: HotWalletPasswordRequester,
private val appRouter: AppRouter,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -57,7 +58,7 @@ internal class LockUserWalletsTimer(
if (shouldOpenWelcomeScreenOnResume) {
passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
appRouter.replaceAll(AppRoute.Welcome())
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
}
@ -121,7 +122,7 @@ internal class LockUserWalletsTimer(
.onRight {
passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
appRouter.replaceAll(AppRoute.Welcome())
}
}
}

View file

@ -19,6 +19,8 @@ import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri
@ -26,6 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -37,7 +40,6 @@ import com.tangem.core.ui.extensions.UserInteractionTracker
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -58,7 +60,6 @@ import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.DeepLinkFactory
@ -101,9 +102,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var injectedTangemSdkManager: TangemSdkManager
@Inject
lateinit var scanCardUseCase: ScanCardUseCase
@Inject
lateinit var settingsRepository: SettingsRepository
@ -120,6 +118,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var appRouterConfig: AppRouterConfig
@Inject
internal lateinit var appRouter: AppRouter
@Inject
internal lateinit var routingComponentFactory: RoutingComponent.Factory
@ -240,7 +241,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
setContent {
CompositionLocalProvider(LocalUserInteractionTracker provides userInteractionTracker) {
routingComponent.Content(Modifier.fillMaxSize())
routingComponent.Content(
Modifier
.fillMaxSize()
.semantics { testTagsAsResourceId = true },
)
}
}
}
@ -259,13 +264,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
userWalletsListRepository = userWalletsListRepository,
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
passwordRequester = passwordRequester,
)
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
scanCardUseCase = scanCardUseCase,
cardSdkConfigRepository = cardSdkConfigRepository,
),
appRouter = appRouter,
)
}

View file

@ -8,59 +8,20 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.ExceptionHandler
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.filter.AppsFlyerEventFilter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.attestation.api.TangemApiServiceSettings
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
@ -69,21 +30,15 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.common.log.TangemLoggingInitializer
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.rekotlin.Store
lateinit var store: Store<AppState>
lateinit var walletsRepository: WalletsRepository
val foregroundActivityObserver = ForegroundActivityObserver
@ -93,12 +48,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val entryPoint: ApplicationEntryPoint
get() = EntryPoints.get(this, ApplicationEntryPoint::class.java)
private val appStateHolder: AppStateHolder
get() = entryPoint.getAppStateHolder()
private val issuersConfigStorage: IssuersConfigStorage
get() = entryPoint.getIssuersConfigStorage()
private val environmentConfig: EnvironmentConfig
get() = entryPoint.getEnvironmentConfig()
@ -108,98 +57,14 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val excludedBlockchainsManager: ExcludedBlockchainsManager
get() = entryPoint.getExcludedBlockchainsManager()
private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager()
private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCardScanningFeatureToggles()
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
private val appCurrencyRepository: AppCurrencyRepository
get() = entryPoint.getAppCurrencyRepository()
private val walletManagersFacade: WalletManagersFacade
get() = entryPoint.getWalletManagersFacade()
private val appThemeModeRepository: AppThemeModeRepository
get() = entryPoint.getAppThemeModeRepository()
private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository()
private val appPreferencesStore: AppPreferencesStore
get() = entryPoint.getAppPreferencesStore()
val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase()
private val walletsRepository: WalletsRepository
get() = entryPoint.getWalletsRepository()
private val oneTimeEventFilter: OneTimeEventFilter
get() = entryPoint.getOneTimeEventFilter()
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
get() = entryPoint.getWasTwinsOnboardingShownUseCase()
private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
get() = entryPoint.getSaveTwinsOnboardingShownUseCase()
private val cardRepository: CardRepository
get() = entryPoint.getCardRepository()
private val tangemSdkLogger: TangemSdkLogger
get() = entryPoint.getTangemSdkLogger()
private val settingsRepository: SettingsRepository
get() = entryPoint.getSettingsRepository()
private val blockchainSDKFactory: BlockchainSDKFactory
get() = entryPoint.getBlockchainSDKFactory()
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getSendFeedbackEmailUseCase()
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase
get() = entryPoint.getWalletMetaInfoUseCase()
private val urlOpener
get() = entryPoint.getUrlOpener()
private val shareManager
get() = entryPoint.getShareManager()
private val appRouter: AppRouter
get() = entryPoint.getAppRouter()
private val tangemAppLoggerInitializer: TangemAppLoggerInitializer
get() = entryPoint.getTangemAppLogger()
private val transactionSignerFactory: TransactionSignerFactory
get() = entryPoint.getTransactionSignerFactory()
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles
get() = entryPoint.getOnboardingV2FeatureToggles()
private val onboardingRepository: OnboardingRepository
get() = entryPoint.getOnboardingRepository()
private val excludedBlockchains: ExcludedBlockchains
get() = entryPoint.getExcludedBlockchains()
private val appLogsStore: AppLogsStore
get() = entryPoint.getAppLogsStore()
private val clipboardManager: ClipboardManager
get() = entryPoint.getClipboardManager()
private val settingsManager: SettingsManager
get() = entryPoint.getSettingsManager()
private val uiMessageSender: UiMessageSender
get() = entryPoint.getUiMessageSender()
private val tangemLoggingInitializer: TangemLoggingInitializer
get() = entryPoint.getTangemLoggingInitializer()
private val blockchainExceptionHandler: BlockchainExceptionHandler
get() = entryPoint.getBlockchainExceptionHandler()
@ -212,35 +77,20 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
.setWorkerFactory(workerFactory)
.build()
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory
get() = entryPoint.getColdUserWalletBuilderFactory()
private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
private val tangemHotSdk
get() = entryPoint.getTangemHotSdk()
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
private val trackingContextProxy
get() = entryPoint.getTrackingContextProxy()
private val abTestsManager: ABTestsManager
get() = entryPoint.getABTestsManager()
private val appsFlyerClientFactory: AppsFlyerClient.Factory
get() = entryPoint.getAppsFlyerClientFactory()
private val customerIoFeatureToggles: CustomerIoFeatureToggles
get() = entryPoint.getCustomerIoFeatureToggles()
private val scanFailsRequester
get() = entryPoint.getScanFailsRequester()
private val sendTransactionSignerInfoInterceptor
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
// endregion
@ -277,14 +127,14 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
* Initialize components that need to be initialized before [super.onCreate] is called
*/
fun preInit() {
tangemAppLoggerInitializer.initialize()
tangemLoggingInitializer.initAppLogging()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
}
fun init() {
apiConfigsManager.initialize()
walletsRepository = entryPoint.getWalletsRepository()
store = createReduxStore()
apiConfigsManager.initialize()
TangemLogger.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
@ -292,107 +142,25 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
TangemLogger.i(excludedBlockchainsManager.toString())
}
initWithConfigDependency(environmentConfig = environmentConfig)
initAnalytics(application = this, environmentConfig = environmentConfig)
abTestsManager.init()
appScope.launch {
launch(Dispatchers.IO) {
loadNativeLibraries()
updateLogFiles()
}
}
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
BlockchainSdkRetrofitBuilder.interceptors = buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
}
TangemApiServiceSettings.addInterceptors(
*buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
add(NetworkLogsSaveInterceptor(appLogsStore))
}.toTypedArray(),
)
}
appStateHolder.mainStore = store
tangemLoggingInitializer.initSdkLogging(this)
wcInitializeUseCase.init(
projectId = environmentConfig.walletConnectProjectId,
)
}
private fun createReduxStore(): Store<AppState> {
return Store(
reducer = { action, state -> appReducer(action, requireNotNull(state)) },
middleware = AppState.getMiddleware(),
state = AppState(
daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager,
cardScanningFeatureToggles = cardScanningFeatureToggles,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
appStateHolder = appStateHolder,
appThemeModeRepository = appThemeModeRepository,
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
issuersConfigStorage = issuersConfigStorage,
urlOpener = urlOpener,
shareManager = shareManager,
appRouter = appRouter,
transactionSignerFactory = transactionSignerFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
onboardingRepository = onboardingRepository,
excludedBlockchains = excludedBlockchains,
appPreferencesStore = appPreferencesStore,
clipboardManager = clipboardManager,
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,
scanFailsRequester = scanFailsRequester,
),
),
)
}
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporarily logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(
context = this,
@ -404,20 +172,13 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
System.loadLibrary("TrustWalletCore")
}
private fun initWithConfigDependency(environmentConfig: EnvironmentConfig) {
initAnalytics(this, environmentConfig)
Log.addLogger(logger = tangemSdkLogger)
}
private fun initAnalytics(application: Application, environmentConfig: EnvironmentConfig) {
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
if (customerIoFeatureToggles.isFeatureEnabled) {
factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder())
}
factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder())
factory.addFilter(oneTimeEventFilter)
factory.addFilter(AppsFlyerEventFilter())
@ -430,23 +191,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
jsonConverter = MoshiConverter.sdkMoshiConverter,
)
Analytics.addParamsInterceptor(
interceptor = object : ParamsInterceptor {
override fun id(): String = "SendTransactionSignerInfoInterceptor"
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.TransactionSent
override fun intercept(params: MutableMap<String, String>) {
val isLastSignWithRing = store.state.globalState.isLastSignWithRing
params[AnalyticsParam.WALLET_FORM] = if (isLastSignWithRing) {
WalletForm.Ring.name
} else {
WalletForm.Card.name
}
}
},
)
Analytics.addParamsInterceptor(interceptor = sendTransactionSignerInfoInterceptor)
factory.build(Analytics, buildData)
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.common
/**
[REDACTED_AUTHOR]
*/
object TestActions {
// It used only for the test actions in debug or debug_beta builds
var isTestAmountInjectionForWalletManagerEnabled = false
}
typealias TestAction = Pair<String, () -> Unit>

View file

@ -1,13 +0,0 @@
package com.tangem.tap.common.analytics
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import javax.inject.Inject
class CustomerIoFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isFeatureEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.CUSTOMER_IO_ENABLED)
}

View file

@ -3,7 +3,9 @@ package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.AnalyticsUserIdHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.wallet.BuildConfig
class AmplitudeAnalyticsHandler(
private val client: AmplitudeAnalyticsClient,
@ -29,10 +31,20 @@ class AmplitudeAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler {
return AmplitudeAnalyticsHandler(
client = if (data.logConfig.isAmplitudeLogEnabled) {
AmplitudeLogClient(data.jsonConverter)
client = if (BuildConfig.TESTER_MENU_ENABLED) {
AmplitudeClient(
application = data.application,
key = requireNotNull(data.config.amplitudeApiKeyDev) {
"Amplitude api key not found in ${BuildConfig.BUILD_TYPE}"
},
logger = AnalyticsEventsLogger(name = ID, jsonConverter = data.jsonConverter),
)
} else {
AmplitudeClient(data.application, data.config.amplitudeApiKey)
AmplitudeClient(
application = data.application,
key = data.config.amplitudeApiKey,
logger = null,
)
},
)
}

View file

@ -5,7 +5,9 @@ import com.amplitude.api.Amplitude
import com.amplitude.api.AmplitudeClient
import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
import com.tangem.utils.converter.Converter
import com.tangem.wallet.BuildConfig
import org.json.JSONObject
/**
@ -16,6 +18,7 @@ interface AmplitudeAnalyticsClient : EventLogger, UserIdHolder
internal class AmplitudeClient(
application: Application,
key: String,
private val logger: AnalyticsEventsLogger?,
) : AmplitudeAnalyticsClient {
private val client: AmplitudeClient = Amplitude.getInstance()
@ -23,6 +26,7 @@ internal class AmplitudeClient(
init {
client.initialize(application, key)
client.enableForegroundTracking(application)
client.enableLogging(BuildConfig.TESTER_MENU_ENABLED)
}
override fun setUserId(userId: String) {
@ -34,6 +38,7 @@ internal class AmplitudeClient(
}
override fun logEvent(event: String, params: Map<String, String>) {
logger?.logEvent(event, params)
client.logEvent(event, ParamsToJSONObjectConverter().convert(params))
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AmplitudeLogClient(
jsonConverter: MoshiJsonConverter,
) : AmplitudeAnalyticsClient {
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter)
private var userId: String? = null
override fun setUserId(userId: String) {
this.userId = userId
}
override fun clearUserId() {
this.userId = null
}
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
@ -10,10 +11,8 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.walletsRepository
import kotlinx.coroutines.runBlocking
/**
@ -23,8 +22,6 @@ class CardContextInterceptor(
private val scanResponse: ScanResponse,
) : ParamsInterceptor {
private val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
private val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
override fun id(): String = CardContextInterceptor.id()
@ -35,6 +32,7 @@ class CardContextInterceptor(
is IntroductionProcess.ButtonScanCardLegacy,
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
is Basic.CardWasScanned,
-> false
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
else -> true

View file

@ -1,24 +0,0 @@
package com.tangem.tap.common.extensions
import android.content.*
import android.content.pm.*
import android.content.res.*
import android.net.*
import androidx.annotation.*
import androidx.core.content.*
/**
* Get uri to any resource type via given Resource Instance
* @param resId - resource id
* @throws Resources.NotFoundException if the given ID does not exist.
* @return - Uri to resource by the given ID
*/
@Throws(Resources.NotFoundException::class)
fun Context.resourceUri(@AnyRes resId: Int): Uri {
return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE +
"://" + resources.getResourcePackageName(resId) +
'/' + resources.getResourceTypeName(resId) +
'/' + resources.getResourceEntryName(resId),
)
}

View file

@ -1,62 +0,0 @@
package com.tangem.tap.common.extensions
import com.tangem.common.routing.AppRouter
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Store
/**
* Dispatch action with creating the new coroutine with the Main dispatcher
*
* @see dispatchWithMain
*/
fun Store<*>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
dispatch(action)
}
}
/**
* Dispatch action on the Main coroutine context
*
* @param action [Action] to be dispatched
*
* @see dispatchOnMain
* */
suspend fun Store<*>.dispatchWithMain(action: Action) {
withMainContext {
dispatch(action)
}
}
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
state.globalState.tapWalletManager.onWalletSelected(userWallet)
}
/**
* Dispatch action inside a coroutine with the Main dispatcher
*/
@Deprecated(
message = "Use dispatchWithMain instead",
replaceWith = ReplaceWith(expression = "dispatchWithMain"),
)
suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
inject(DaggerGraphState::appRouter).action()
}
inline fun <reified T> Store<AppState>.inject(getDependency: DaggerGraphState.() -> T?): T {
return requireNotNull(state.daggerGraphState.getDependency()) {
"${T::class.simpleName.orEmpty()} isn't initialized "
}
}

View file

@ -1,56 +1,11 @@
@file:Suppress("TooManyFunctions")
package com.tangem.tap.common.extensions
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableResId)
}
@ColorInt
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
@ColorInt
fun View.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(context, colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
return if (show) this.show(invokeBeforeStateChanged) else this.hide(invokeBeforeStateChanged)
}
fun View.show(invokeBeforeStateChanged: (() -> Unit)? = null) {
if (this.visibility == View.VISIBLE) return
invokeBeforeStateChanged?.invoke()
this.visibility = View.VISIBLE
}
fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
if (this.visibility == View.GONE) return
invokeBeforeStateChanged?.invoke()
this.visibility = View.GONE
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, *formatArgs)
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.common.services.Result
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.delay
/**
[REDACTED_AUTHOR]
*/
@Deprecated(
message = "Use WalletStoresManager.fetch({userWalletId}, refresh = true) (to update all user wallet tokens)" +
"or WalletCurrenciesManager.update(...) (to update only one user wallet blockchain and its tokens) instead",
)
@Suppress("MagicNumber")
suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try {
if (isDemoCard || TestActions.isTestAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.isTestAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)
} else {
update()
Result.Success(wallet)
}
} catch (exception: Exception) {
TangemLogger.e("Error", exception)
val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
Result.Failure(TapError.NoInternetConnection())
} else {
val blockchain = wallet.blockchain
val amountToCreateAccount = blockchain.amountToCreateAccount(this, wallet.getFirstToken())
if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) {
Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString()))
} else {
when (exception) {
is BlockchainSdkError -> Result.Failure(exception)
else -> {
val message = exception.cause?.localizedMessage ?: "Unknown error"
Result.Failure(TapError.WalletManager.InternalError(message))
}
}
}
}
}

View file

@ -3,13 +3,15 @@ package com.tangem.tap.common.libs.blockchainsdk
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
import com.tangem.core.analytics.store.LastSignedWalletFormStore
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.domain.card.models.TwinKey
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.store
internal class DefaultTransactionSignerFactory : TransactionSignerFactory {
internal class DefaultTransactionSignerFactory(
private val lastSignedWalletFormStore: LastSignedWalletFormStore,
) : TransactionSignerFactory {
override fun createTransactionSigner(cardId: String?, sdk: TangemSdk, twinKey: TwinKey?): TransactionSigner {
return TangemSigner(
@ -18,7 +20,9 @@ internal class DefaultTransactionSignerFactory : TransactionSignerFactory {
initialMessage = Message(),
twinKey = twinKey,
) { signResponse ->
store.dispatch(action = GlobalAction.IsSignWithRing(signResponse.isRing))
lastSignedWalletFormStore.update(
if (signResponse.isRing) WalletForm.Ring else WalletForm.Card,
)
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.common.log
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.utils.logging.Severity
import com.tangem.utils.logging.TangemLogger
/**
* [TangemLogger.LogWriter] that persists log entries to [AppLogsStore].
*
* Only [Severity.Error] and [Severity.Info] are written. The `shouldSanitize` flag is forwarded to
* [AppLogsStore.saveLogMessage], so callers that deliberately log unsanitized content
* (`shouldSanitize = false`) bypass the sanitizer.
*/
internal class FileLogWriter(
private val appLogsStore: AppLogsStore,
) : TangemLogger.LogWriter {
override fun isLoggable(severity: Severity, tag: String): Boolean {
return severity == Severity.Error || severity == Severity.Info
}
override fun write(
severity: Severity,
tag: String,
message: String,
throwable: Throwable?,
shouldSanitize: Boolean,
) {
appLogsStore.saveLogMessage(
tag = tag,
message = message,
throwable = throwable,
shouldSanitize = shouldSanitize,
)
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.tap.common.log
import android.os.Build
import android.util.Log
import com.tangem.utils.logging.Severity
import com.tangem.utils.logging.TangemLogger
/**
* [TangemLogger.LogWriter] that pretty-prints log entries to Logcat.
*
* Wraps each entry in unicode borders and chunks long messages so that they fit
* Android's per-entry byte limit (~4076 bytes).
*/
internal class LogcatLogWriter : TangemLogger.LogWriter {
override fun write(
severity: Severity,
tag: String,
message: String,
throwable: Throwable?,
shouldSanitize: Boolean,
) {
val priority = severity.toAndroidPriority()
val truncatedTag = tag.truncateForLogcat()
val finalMessage = if (throwable != null) {
"$message\n${Log.getStackTraceString(throwable)}"
} else {
message
}
printBoxed(priority, truncatedTag, finalMessage)
}
private fun printBoxed(priority: Int, tag: String, message: String) {
Log.println(priority, tag, TOP_BORDER)
val bytes = message.toByteArray()
val length = bytes.size
if (length <= CHUNK_SIZE) {
printContent(priority, tag, message)
} else {
var i = 0
while (i < length) {
val count = (length - i).coerceAtMost(CHUNK_SIZE)
printContent(priority, tag, String(bytes, i, count))
i += CHUNK_SIZE
}
}
Log.println(priority, tag, BOTTOM_BORDER)
}
private fun printContent(priority: Int, tag: String, chunk: String) {
chunk.split(System.lineSeparator()).forEach { line ->
Log.println(priority, tag, "$HORIZONTAL_LINE $line")
}
}
@Suppress("MagicNumber")
private fun String.truncateForLogcat(): String {
// Tag length limit was removed in API 26.
return if (length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
this
} else {
substring(0, MAX_TAG_LENGTH)
}
}
private fun Severity.toAndroidPriority(): Int = when (this) {
Severity.Verbose -> Log.VERBOSE
Severity.Debug -> Log.DEBUG
Severity.Info -> Log.INFO
Severity.Warn -> Log.WARN
Severity.Error -> Log.ERROR
Severity.Assert -> Log.ASSERT
}
private companion object {
// Android's max per-entry byte limit is ~4076; leave headroom for borders.
const val CHUNK_SIZE = 4000
const val MAX_TAG_LENGTH = 23
const val HORIZONTAL_LINE = ""
const val DIVIDER = "────────────────────────────────────────────────────────"
const val TOP_BORDER = "$DIVIDER$DIVIDER"
const val BOTTOM_BORDER = "$DIVIDER$DIVIDER"
}
}

View file

@ -1,121 +0,0 @@
package com.tangem.tap.common.log
import android.os.Build
import android.util.Log
import co.touchlab.kermit.BaseLogger
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.orhanobut.logger.AndroidLogAdapter
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import java.util.regex.Pattern
import com.orhanobut.logger.Logger as PrettyLogger
/**
* Tangem app logger
*
* @property appLogsStore app logs store
*
[REDACTED_AUTHOR]
*/
class TangemAppLoggerInitializer(
private val appLogsStore: AppLogsStore,
) {
/** Initialize */
fun initialize() {
if (IS_LOG_ENABLED) {
PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
}
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
}
private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) {
if (IS_LOG_ENABLED) {
PrettyLogger.log(priority, tag, message, t)
}
if (PERMITTED_PRIORITY.contains(priority)) {
appLogsStore.saveLogMessage(
tag = tag ?: "TangemAppLogger",
message = message,
)
}
}
@Suppress("BooleanPropertyNaming")
private companion object {
val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED
val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO)
}
}
private class KermitLogWriter(
private val finalLogOutput: (priority: Int, tag: String?, message: String, t: Throwable?) -> Unit,
) : LogWriter() {
private val fqcnIgnore = setOf(
LogWriter::class.java.name,
KermitLogWriter::class.java.name,
BaseLogger::class.java.name,
Logger::class.java.name,
TangemLogger::class.java.name,
TangemLogger.TaggedLogger::class.java.name,
)
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
val priority = when (severity) {
Severity.Verbose -> PrettyLogger.VERBOSE
Severity.Debug -> PrettyLogger.DEBUG
Severity.Info -> PrettyLogger.INFO
Severity.Warn -> PrettyLogger.WARN
Severity.Error -> PrettyLogger.ERROR
Severity.Assert -> PrettyLogger.ASSERT
}
val finalTag = if (tag != KERMIT_LOGGER_DEFAULT_TAG) {
tag
} else {
/**
* like in [Logger.debugTree.tag]
*/
@Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause")
Throwable().stackTrace
.first { it.className !in fqcnIgnore }
.let(::createStackElementTag)
}
finalLogOutput(priority, finalTag, message, throwable)
}
/**
* copy from [Logger.debugTree.createStackElementTag]
*/
@Suppress("MagicNumber")
private fun createStackElementTag(element: StackTraceElement): String? {
var tag = element.className.substringAfterLast('.')
val m = ANONYMOUS_CLASS.matcher(tag)
if (m.find()) {
tag = m.replaceAll("")
}
// Tag length limit was removed in API 26.
return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
tag
} else {
tag.substring(0, MAX_TAG_LENGTH)
}
}
private companion object {
private const val KERMIT_LOGGER_DEFAULT_TAG = ""
/**
* copy from [Logger.debugTree.Companion]
*/
private const val MAX_TAG_LENGTH = 23
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.data
package com.tangem.tap.common.log
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.logs.AppLogsStore

View file

@ -6,27 +6,40 @@ import com.tangem.TangemSdkLogger
import com.tangem.datasource.local.logs.AppLogsStore
/**
* CardSDK logger implementation
* CardSDK logger implementation.
*
* @property levels logging levels
* @property messageFormatter message formatter
* @property appLogsStore app logs store
* @property appLogsStore app logs store
*
[REDACTED_AUTHOR]
*/
@Suppress("UnusedPrivateMember")
internal class TangemCardSDKLogger(
private val levels: List<Log.Level>,
private val messageFormatter: LogFormat,
private val appLogsStore: AppLogsStore,
) : TangemSdkLogger {
private val messageFormatter: LogFormat = LogFormat.StairsFormatter()
override fun log(message: () -> String, level: Log.Level) {
if (!levels.contains(level)) return
if (!LEVELS.contains(level)) return
appLogsStore.saveLogMessage(
tag = "CardSDK_${level.name}",
message = messageFormatter.format(message = message, level = level),
)
}
private companion object {
val LEVELS = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
Log.Level.Info,
)
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.tap.common.log
import android.app.Application
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.common.LogConfig
import com.tangem.operations.attestation.api.TangemApiServiceSettings
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
/**
* Owns all app-startup wiring of the logging subsystem in a single place:
* - [initAppLogging] registers [TangemLogger] writers (Logcat + file).
* - [initSdkLogging] registers the Card SDK logger with [Log] and installs OkHttp
* interceptors for the Blockchain SDK and the Tangem API.
*
* @property appLogsStore app logs store used by file-based writer and the network logs save
* interceptor
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
*
[REDACTED_AUTHOR]
*/
class TangemLoggingInitializer(
private val appLogsStore: AppLogsStore,
private val tangemSdkLogger: TangemSdkLogger,
) {
fun initAppLogging() {
TangemLogger.setLogWriters(
buildList {
if (BuildConfig.LOG_ENABLED) {
add(LogcatLogWriter())
}
add(FileLogWriter(appLogsStore))
},
)
}
/**
* Configure logging for the underlying SDKs:
* - register [tangemSdkLogger] with the Card SDK static [Log] facade,
* - install OkHttp interceptors for the Blockchain SDK and Tangem API.
*
* Must be called from `TangemApplication.init()` AFTER `entryPoint.getWalletsRepository()`
* has triggered Hilt singletons construction in particular `DefaultCardSdkProvider`,
* whose init block registers `AddHeadersInterceptor` in [TangemApiServiceSettings].
* Calling this method earlier would invert the OkHttp interceptor chain order and
* cause logging interceptors to see requests *without* auth headers.
*/
fun initSdkLogging(application: Application) {
Log.addLogger(logger = tangemSdkLogger)
if (!LogConfig.network.isBlockchainSdkNetworkLogEnabled) return
BlockchainSdkRetrofitBuilder.interceptors = buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(application))
}
TangemApiServiceSettings.addInterceptors(
*buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(application))
add(NetworkLogsSaveInterceptor(appLogsStore))
}.toTypedArray(),
)
}
}

View file

@ -1,63 +0,0 @@
package com.tangem.tap.common.log
import com.orhanobut.logger.FormatStrategy
import com.orhanobut.logger.LogStrategy
import com.orhanobut.logger.LogcatLogStrategy
class TimberFormatStrategy : FormatStrategy {
private val logStrategy: LogStrategy = LogcatLogStrategy()
override fun log(priority: Int, tag: String?, message: String) {
logTopBorder(priority, tag)
val bytes = message.toByteArray()
val length = bytes.size
if (length <= CHUNK_SIZE) {
logContent(priority, tag, message)
logBottomBorder(priority, tag)
return
}
var i = 0
while (i < length) {
val count = (length - i).coerceAtMost(CHUNK_SIZE)
// create a new String with system's default charset (which is UTF-8 for Android)
logContent(priority, tag, String(bytes, i, count))
i += CHUNK_SIZE
}
logBottomBorder(priority, tag)
}
private fun logTopBorder(logType: Int, tag: String?) {
logChunk(logType, tag, TOP_BORDER)
}
private fun logBottomBorder(logType: Int, tag: String?) {
logChunk(logType, tag, BOTTOM_BORDER)
}
private fun logContent(logType: Int, tag: String?, chunk: String) {
chunk.split(System.lineSeparator()).forEach { line ->
logChunk(logType, tag, "$HORIZONTAL_LINE $line")
}
}
private fun logChunk(priority: Int, tag: String?, chunk: String) {
logStrategy.log(priority, tag, chunk)
}
private companion object {
/**
* Android's max limit for a log entry is ~4076 bytes,
* so 4000 bytes is used as chunk size since default charset
* is UTF-8
*/
private const val CHUNK_SIZE = 4000
const val TOP_LEFT_CORNER = ""
const val BOTTOM_LEFT_CORNER = ""
const val HORIZONTAL_LINE = ""
const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────"
const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
}
}

View file

@ -3,19 +3,12 @@ package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.AndroidEntryPoint
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import javax.inject.Inject
@AndroidEntryPoint
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
@Inject
lateinit var customerIoFeatureToggles: CustomerIoFeatureToggles
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
@ -24,21 +17,17 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
super.onNewToken(token)
TangemLogger.d("New FCM token received: $token")
if (customerIoFeatureToggles.isFeatureEnabled) {
CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)
}
CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
if (customerIoFeatureToggles.isFeatureEnabled) {
CustomerIOFirebaseMessagingService.onMessageReceived(
context = applicationContext,
remoteMessage = message,
handleNotificationTrigger = false,
)
}
CustomerIOFirebaseMessagingService.onMessageReceived(
context = applicationContext,
remoteMessage = message,
handleNotificationTrigger = false,
)
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID

View file

@ -1,33 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class AccessCodeRequestPolicyMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
if (action is GlobalAction.SaveScanResponse) {
updateAccessCodeRequestPolicy(action.scanResponse)
}
next(action)
}
}
}
private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
mainScope.launch {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet,
)
}
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}
sealed class AppAction : Action {
data class RestoreState(val state: AppState) : AppAction()
}

View file

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

View file

@ -1,15 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.tap.lockUserWalletsTimer
import org.rekotlin.Middleware
class LockUserWalletsTimerMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
lockUserWalletsTimer?.restart()
nextDispatch(action)
}
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.utils.logging.TangemLogger
import org.rekotlin.Middleware
/**
[REDACTED_AUTHOR]
*/
val logMiddleware: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
TangemLogger.i("Dispatch action: ${action::class.java.simpleName}")
nextDispatch(action)
}
}
}

View file

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

View file

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

View file

@ -1,25 +0,0 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
@Suppress("LongMethod", "ComplexMethod")
fun globalReducer(action: Action, state: AppState): GlobalState {
if (action !is GlobalAction) return state.globalState
val globalState = state.globalState
return when (action) {
is GlobalAction.SaveScanResponse -> {
globalState.copy(scanResponse = action.scanResponse)
}
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.RestoreAppCurrency.Success -> {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
else -> globalState
}
}

View file

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

View file

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

View file

@ -1,11 +1,11 @@
package com.tangem.tap.core.navigation.email
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.foregroundActivityObserver
import com.tangem.utils.logging.TangemLogger
@ -15,7 +15,9 @@ import com.tangem.utils.logging.TangemLogger
*
[REDACTED_AUTHOR]
*/
internal class AndroidEmailSender : EmailSender {
internal class AndroidEmailSender(
private val messageTruncator: EmailMessageTruncator,
) : EmailSender {
override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) {
val activity = foregroundActivityObserver.foregroundActivity
@ -26,7 +28,7 @@ internal class AndroidEmailSender : EmailSender {
}
val originalIntent = createEmailShareIntent(activity, email)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, "mailto:".toUri())
val packageManager = activity.packageManager
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
@ -59,7 +61,7 @@ internal class AndroidEmailSender : EmailSender {
.setType("message/rfc822")
.setEmailTo(arrayOf(email.address))
.setSubject(email.subject)
.setText(email.message)
.setText(messageTruncator.truncate(email.message))
email.attachment?.let { file ->
builder.setStream(

View file

@ -0,0 +1,35 @@
package com.tangem.tap.core.navigation.email
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
/**
* Truncates an email body so the resulting Intent fits inside the per-process Binder buffer (1 MB).
*
* The chooser fans the Intent out to every installed email client (with extras duplicated per target),
* so the body must be kept well below the raw 1 MB ceiling.
*/
internal class EmailMessageTruncator {
fun truncate(message: String): String {
val bytes = message.toByteArray(Charsets.UTF_8)
if (bytes.size <= MAX_MESSAGE_BYTES) return message
val suffix = TRUNCATION_SUFFIX_TEMPLATE.format(bytes.size)
val suffixBytes = suffix.toByteArray(Charsets.UTF_8).size
val cutSize = MAX_MESSAGE_BYTES - suffixBytes
// Drop a partial UTF-8 sequence at the cut boundary rather than replacing it with U+FFFD
// (which is 3 bytes in UTF-8 and would push the result over the cap).
val decoder = Charsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.IGNORE)
val head = decoder.decode(ByteBuffer.wrap(bytes, 0, cutSize)).toString()
return head + suffix
}
private companion object {
// Chooser duplicates EXTRA_TEXT once per target email app (EXTRA_INITIAL_INTENTS),
// so parcel ≈ N × body. 20 KB clears the 1 MB Binder limit for up to ~30 mail clients.
const val MAX_MESSAGE_BYTES = 20_000
const val TRUNCATION_SUFFIX_TEMPLATE = "\n\n…[truncated, original %d bytes]"
}
}

View file

@ -18,8 +18,9 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
val isPatched by lazy { hasSecurityPatch() }
val isVulnerable = isAffected && !isPatched
TangemLogger.i(
"CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " +
messageString = "CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " +
"isPatched=$isPatched, isVulnerable=$isVulnerable",
shouldSanitize = false,
)
isVulnerable
}
@ -27,7 +28,10 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
private fun isAffectedMediaTekDevice(): Boolean {
val socModel = resolveMediaTekSocModel()
val isAffected = socModel != null && socModel in AFFECTED_MEDIATEK_SOCS
TangemLogger.i("CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected")
TangemLogger.i(
messageString = "CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected",
shouldSanitize = false,
)
return isAffected
}
@ -36,7 +40,10 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manufacturer = Build.SOC_MANUFACTURER
val model = Build.SOC_MODEL
TangemLogger.i("CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model")
TangemLogger.i(
messageString = "CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model",
shouldSanitize = false,
)
if (manufacturer.equals("MediaTek", ignoreCase = true)) {
extractSocModel(model)?.let { return it }
}
@ -44,7 +51,7 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
// Layer 2: Build.HARDWARE often contains "mtXXXX" on MediaTek devices (public API)
val hardware = Build.HARDWARE
TangemLogger.i("CVE-2026-20435 Layer 2: HARDWARE=$hardware")
TangemLogger.i(messageString = "CVE-2026-20435 Layer 2: HARDWARE=$hardware", shouldSanitize = false)
extractSocModel(hardware)?.let { return it }
return null

View file

@ -1,13 +0,0 @@
package com.tangem.tap.core.ui
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import javax.inject.Inject
class DefaultHoldToConfirmButtonFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : HoldToConfirmButtonFeatureToggles {
override val isHoldToConfirmEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.HOLD_TO_CONFIRM_BUTTON_ENABLED)
}

View file

@ -1,26 +1,27 @@
package com.tangem.datasource.info
package com.tangem.tap.data
import android.os.Build
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import java.util.*
import com.tangem.wallet.BuildConfig
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
internal class AndroidAppInfoProvider @Inject constructor(
private val appVersionProvider: AppVersionProvider,
) : AppInfoProvider {
internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
override val platform: String
get() = "Android"
override val device: String
get() = "${Build.MANUFACTURER} ${Build.MODEL}"
override val osVersion: String
get() = Build.VERSION.RELEASE
override val sdkVersion: Int
get() = Build.VERSION.SDK_INT
override val language: String
get() = Locale.getDefault().language
get() = Locale.getDefault().toLanguageTag()
override val timezone: String
get() = TimeZone.getDefault().id
override val appVersion: String
get() = appVersionProvider.versionName
override val appVersion: String = BuildConfig.VERSION_NAME
override val appVersionCode: Int = BuildConfig.VERSION_CODE
override val isHuaweiDevice: Boolean
get() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true) ||
Build.BRAND.equals("HUAWEI", ignoreCase = true)

View file

@ -32,7 +32,6 @@ import com.tangem.tap.foregroundActivityObserver
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton
@ -48,7 +47,6 @@ internal class DefaultCardSdkProvider @Inject constructor(
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val apiConfigsManager: ApiConfigsManager,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
authProvider: AuthProvider,
) : CardSdkProvider, CardSdkOwner {
@ -74,10 +72,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
val apiEnvironment = Provider {
apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemTech).environment
}
val platformHeaders = RequestHeader.AppVersionPlatformHeaders(
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
val platformHeaders = RequestHeader.AppVersionPlatformHeaders(appInfoProvider)
val apiKeyHeader = RequestHeader.TangemApiKeyHeader(authProvider, apiEnvironment)
TangemApiServiceSettings.addInterceptors(
AddHeadersInterceptor(platformHeaders.values + apiKeyHeader.values),

View file

@ -4,7 +4,6 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -14,8 +13,7 @@ import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -45,13 +43,13 @@ internal object ActivityModule {
@Provides
@Singleton
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
sellService: SellService,
expressServiceFetcher: ExpressServiceFetcher,
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
sellService = Provider { requireNotNull(appStateHolder.sellService) },
sellService = sellService,
expressServiceFetcher = expressServiceFetcher,
currenciesRepository = currenciesRepository,
dispatchers = dispatchers,

View file

@ -1,18 +0,0 @@
package com.tangem.tap.di
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.tap.proxy.AppStateHolder
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppStateHolderModule {
@Binds
@Singleton
fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di
import android.content.Context
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.domain.card.BuildConfig
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -36,6 +37,7 @@ internal class TangemSdkManagerModule {
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
blockchainToDeriveFinder: BlockchainToDeriveFinder,
analyticsErrorHandler: AnalyticsErrorHandler,
cardRepository: CardRepository,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources)
@ -50,6 +52,7 @@ internal class TangemSdkManagerModule {
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
blockchainToDeriveFinder = blockchainToDeriveFinder,
analyticsErrorHandler = analyticsErrorHandler,
cardRepository = cardRepository,
)
}
}

View file

@ -13,6 +13,8 @@ import com.tangem.tap.common.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager
import com.tangem.tap.common.url.CustomTabsUrlOpener
import com.tangem.tap.core.DefaultAppCoroutineScope
import com.tangem.tap.data.DefaultAppInfoProvider
import com.tangem.utils.info.AppInfoProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -28,6 +30,10 @@ internal interface UtilsModule {
@Binds
fun provideAppScope(defaultAppScope: DefaultAppCoroutineScope): AppCoroutineScope
@Binds
@Singleton
fun bindAppInfoProvider(impl: DefaultAppInfoProvider): AppInfoProvider
companion object {
@Provides

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.core.navigation.email
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.core.navigation.email.AndroidEmailSender
import com.tangem.tap.core.navigation.email.EmailMessageTruncator
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,5 +15,7 @@ internal object EmailSenderModule {
@Provides
@Singleton
fun provideEmailSender(): EmailSender = AndroidEmailSender()
fun provideEmailSender(): EmailSender = AndroidEmailSender(
messageTruncator = EmailMessageTruncator(),
)
}

View file

@ -1,9 +1,7 @@
package com.tangem.tap.di.core.ui
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.tap.core.ui.DefaultDesignFeatureToggles
import com.tangem.tap.core.ui.DefaultHoldToConfirmButtonFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -15,9 +13,4 @@ interface CoreUiBindsModule {
@Binds
fun bindDesignFeatureToggles(impl: DefaultDesignFeatureToggles): DesignFeatureToggles
@Binds
fun bindHoldToConfirmButtonFeatureToggles(
impl: DefaultHoldToConfirmButtonFeatureToggles,
): HoldToConfirmButtonFeatureToggles
}

View file

@ -1,12 +1,18 @@
package com.tangem.tap.di.data
import android.content.Context
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.data.DefaultCardSdkProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.File
import javax.inject.Singleton
@Module
@ -20,4 +26,22 @@ internal interface CardSdkModule {
@Binds
@Singleton
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner
companion object {
@Provides
@Singleton
fun provideCardArtworksProvider(
sdkRepository: CardSdkConfigRepository,
@ApplicationContext context: Context,
): CardArtworksProvider {
return CardArtworksProvider(
tangemApiBaseUrlProvider = { sdkRepository.sdk.config.tangemApiBaseUrl },
artworksDirectory = File(
context.getExternalFilesDir(null) ?: context.filesDir,
"card_artworks",
).apply { mkdirs() },
)
}
}
}

View file

@ -1,13 +1,10 @@
package com.tangem.tap.di.data
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.log.TangemBlockchainSDKLogger
import com.tangem.tap.common.log.TangemCardSDKLogger
import com.tangem.tap.data.TangemBlockchainSDKLogger
import com.tangem.tap.common.log.TangemLoggingInitializer
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,31 +17,10 @@ internal object TangemLoggingModule {
@Provides
@Singleton
fun provideAppLoggerInitializer(appLogsStore: AppLogsStore): TangemAppLoggerInitializer {
return TangemAppLoggerInitializer(appLogsStore)
}
@Provides
@Singleton
fun provideCardSDKLogger(appLogsStore: AppLogsStore): TangemSdkLogger {
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
Log.Level.Info,
)
return TangemCardSDKLogger(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(),
fun provideLoggingInitializer(appLogsStore: AppLogsStore): TangemLoggingInitializer {
return TangemLoggingInitializer(
appLogsStore = appLogsStore,
tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
)
}

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