Updated on 2026-08-14
This commit is contained in:
commit
1146554992
1461 changed files with 62266 additions and 17096 deletions
320
.claude/skills/cleanup-feature-toggles/SKILL.md
Normal file
320
.claude/skills/cleanup-feature-toggles/SKILL.md
Normal 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`
|
||||
```
|
||||
|
|
@ -61,7 +61,7 @@ android {
|
|||
}
|
||||
|
||||
flavorDimensions += "services"
|
||||
|
||||
|
||||
productFlavors {
|
||||
create("google") {
|
||||
dimension = "services"
|
||||
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -181,4 +191,70 @@ fun BaseTestCase.openSendConfirmScreenViaNextButton() {
|
|||
step("Assert 'Send' button on 'Send confirm' screen is displayed") {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 "
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/com/tangem/tap/common/log/FileLogWriter.kt
Normal file
36
app/src/main/java/com/tangem/tap/common/log/FileLogWriter.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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+)+$")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue