Updated on 2026-08-14
This commit is contained in:
commit
47dc1c38df
762 changed files with 23960 additions and 10924 deletions
23
.idea/file.template.settings.xml
generated
23
.idea/file.template.settings.xml
generated
|
|
@ -1,10 +1,13 @@
|
|||
<application>
|
||||
<component name="ExportableFileTemplateSettings">
|
||||
<includes_templates>
|
||||
<template name="Insert package.java" file-name="InsertPackage" reformat="true" live-template-enabled="false" />
|
||||
</includes_templates>
|
||||
<default_templates>
|
||||
<template name="Decompose component.kt" file-name="${NAME}Component" reformat="true" live-template-enabled="false">
|
||||
<template name="Decompose component.kt.child.5.kt" file-name="di/${NAME}ModelModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.4.kt" file-name="di/${NAME}ComponentModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.0.kt" file-name="entity/${NAME}UM" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.4.kt" file-name="di/${NAME}ComponentModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.5.kt" file-name="di/${NAME}ModelModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.1.kt" file-name="ui/${NAME}Content" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.2.kt" file-name="model/${NAME}Model" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.3.kt" file-name="Default${NAME}Component" reformat="true" live-template-enabled="false" />
|
||||
|
|
@ -16,17 +19,19 @@
|
|||
<template name="Decompose component.kt.child.4.kt" file-name="di/${NAME}ComponentModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="Decompose component.kt.child.5.kt" file-name="di/${NAME}ModelModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FeatureToggles.kt" file-name="${NAME}FeatureToggles" reformat="true" live-template-enabled="false">
|
||||
<template name="FeatureToggles.kt.child.0.kt" file-name="Default${NAME}FeatureToggles" reformat="true" live-template-enabled="false" />
|
||||
<template name="FeatureToggles.kt.child.0.kt" file-name="di/${NAME}FeatureTogglesModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FeatureToggles.kt.child.1.kt" file-name="Default${NAME}FeatureToggles" reformat="true" live-template-enabled="false" />
|
||||
</template>
|
||||
<template name="FeatureToggles.kt.child.0.kt" file-name="Default${NAME}FeatureToggles" reformat="true" live-template-enabled="false" />
|
||||
<template name="FeatureToggles.kt.child.0.kt" file-name="di/${NAME}FeatureTogglesModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FeatureToggles.kt.child.1.kt" file-name="Default${NAME}FeatureToggles" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt" file-name="producer/${NAME}Producer" reformat="true" live-template-enabled="false">
|
||||
<template name="FlowPackaging.kt.child.0.kt" file-name="di/${NAME}FetcherModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.1.kt" file-name="di/${NAME}ProducerFactoryModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.2.kt" file-name="di/${NAME}SupplierModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.3.kt" file-name="fetcher/Default${NAME}Fetcher" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.4.kt" file-name="producer/Default${NAME}Producer" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.5.kt" file-name="fetcher/${NAME}Fetcher" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.1.kt" file-name="di/${NAME}ProducerFactoryModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.0.kt" file-name="di/${NAME}FetcherModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.4.kt" file-name="producer/Default${NAME}Producer" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.3.kt" file-name="fetcher/Default${NAME}Fetcher" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.6.kt" file-name="supplier/${NAME}Supplier" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.2.kt" file-name="di/${NAME}SupplierModule" reformat="true" live-template-enabled="false" />
|
||||
</template>
|
||||
<template name="FlowPackaging.kt.child.0.kt" file-name="di/${NAME}FetcherModule" reformat="true" live-template-enabled="false" />
|
||||
<template name="FlowPackaging.kt.child.1.kt" file-name="di/${NAME}ProducerFactoryModule" reformat="true" live-template-enabled="false" />
|
||||
|
|
|
|||
7
.idea/fileTemplates/FeatureToggles.kt
generated
7
.idea/fileTemplates/FeatureToggles.kt
generated
|
|
@ -1,9 +1,4 @@
|
|||
#if ($TOGGLE_NAME && $TOGGLE_NAME != "")
|
||||
#set($TOGGLE = $TOGGLE_NAME)
|
||||
#else
|
||||
#set($TOGGLE = "TODO")
|
||||
#end
|
||||
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
interface ${NAME}FeatureToggles {
|
||||
|
||||
|
|
|
|||
26
.idea/fileTemplates/FeatureToggles.kt.child.0.kt
generated
26
.idea/fileTemplates/FeatureToggles.kt.child.0.kt
generated
|
|
@ -1,16 +1,18 @@
|
|||
#if ($TOGGLE_NAME && $TOGGLE_NAME != "")
|
||||
#set($TOGGLE = $TOGGLE_NAME)
|
||||
#else
|
||||
#set($TOGGLE = "TODO")
|
||||
#end
|
||||
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}.di#end
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import ${PACKAGE_NAME}.Default${NAME}FeatureToggles
|
||||
import ${PACKAGE_NAME}.${NAME}FeatureToggles
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
internal class Default${NAME}FeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ${NAME}FeatureToggles {
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ${NAME}FeatureTogglesModule {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "$TOGGLE")
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bind${NAME}FeatureToggles(impl: Default${NAME}FeatureToggles): ${NAME}FeatureToggles
|
||||
}
|
||||
13
.idea/fileTemplates/FeatureToggles.kt.child.1.kt
generated
Normal file
13
.idea/fileTemplates/FeatureToggles.kt.child.1.kt
generated
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class Default${NAME}FeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ${NAME}FeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.)
|
||||
}
|
||||
|
|
@ -160,6 +160,7 @@ dependencies {
|
|||
implementation(projects.domain.hotWallet)
|
||||
implementation(projects.domain.news)
|
||||
implementation(projects.domain.earn)
|
||||
implementation(projects.domain.tokensync)
|
||||
|
||||
implementation(projects.common)
|
||||
implementation(projects.common.routing)
|
||||
|
|
@ -189,6 +190,7 @@ dependencies {
|
|||
implementation(projects.data.common)
|
||||
implementation(projects.data.settings)
|
||||
implementation(projects.data.tokens)
|
||||
implementation(projects.data.tokensync)
|
||||
implementation(projects.data.txhistory)
|
||||
implementation(projects.data.wallets)
|
||||
implementation(projects.data.analytics)
|
||||
|
|
@ -252,6 +254,8 @@ dependencies {
|
|||
implementation(projects.features.markets.impl)
|
||||
implementation(projects.features.feed.api)
|
||||
implementation(projects.features.feed.impl)
|
||||
implementation(projects.features.promoBanners.api)
|
||||
implementation(projects.features.promoBanners.impl)
|
||||
implementation(projects.features.onramp.api)
|
||||
implementation(projects.features.onramp.impl)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
|
|
@ -366,6 +370,7 @@ dependencies {
|
|||
implementation(deps.googlePlay.advertising)
|
||||
coreLibraryDesugaring(deps.desugar)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kermit)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.zxing.qrCore)
|
||||
implementation(deps.coil)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.common
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.tap.ApplicationEntryPoint
|
||||
import com.tangem.tap.TangemApplication
|
||||
import dagger.hilt.android.testing.OnComponentReadyRunner
|
||||
|
|
@ -8,15 +9,16 @@ import org.junit.rules.TestRule
|
|||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.Field
|
||||
|
||||
class ApplicationInjectionExecutionRule(
|
||||
private val toggleStates: Map<String, Boolean>
|
||||
private val toggleStates: Map<String, Boolean>,
|
||||
) : TestRule {
|
||||
|
||||
private val tangemApplication: TangemApplication
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
private var originalFeatureTogglesValues: Map<String, String>? = null
|
||||
private var originalVersionValues: Map<FeatureToggles, String>? = null
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
|
|
@ -25,7 +27,7 @@ class ApplicationInjectionExecutionRule(
|
|||
overrideFeatureToggles()
|
||||
|
||||
OnComponentReadyRunner.addListener(
|
||||
tangemApplication, ApplicationEntryPoint::class.java
|
||||
tangemApplication, ApplicationEntryPoint::class.java,
|
||||
) { _: ApplicationEntryPoint ->
|
||||
tangemApplication.preInit()
|
||||
tangemApplication.init()
|
||||
|
|
@ -40,42 +42,25 @@ class ApplicationInjectionExecutionRule(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun saveOriginalFeatureToggles() {
|
||||
try {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
originalFeatureTogglesValues = valuesField.get(null) as Map<String, String>
|
||||
originalVersionValues = FeatureToggles.entries.associateWith { it.version }
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to save original toggles values: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun overrideFeatureToggles() {
|
||||
try {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
val versionField = getVersionField()
|
||||
|
||||
val originalValues = originalFeatureTogglesValues ?:
|
||||
(valuesField.get(null) as Map<String, String>)
|
||||
|
||||
val newValues = originalValues.toMutableMap()
|
||||
|
||||
toggleStates.forEach { (toggle, enabled) ->
|
||||
if (enabled) {
|
||||
newValues[toggle] = "1.0.0"
|
||||
} else {
|
||||
newValues.remove(toggle)
|
||||
}
|
||||
FeatureToggles.entries.forEach { toggle ->
|
||||
val enabled = toggleStates[toggle.rawName] ?: return@forEach
|
||||
val newVersion = if (enabled) ENABLED_VERSION else DISABLED_VERSION
|
||||
versionField.set(toggle, newVersion)
|
||||
}
|
||||
|
||||
valuesField.set(null, newValues)
|
||||
|
||||
Timber.i("FeatureToggles.values updated: $toggleStates")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e("FeatureToggles.values didn't change with error: ${e.message}")
|
||||
}
|
||||
|
|
@ -83,15 +68,27 @@ class ApplicationInjectionExecutionRule(
|
|||
|
||||
private fun restoreOriginalFeatureToggles() {
|
||||
try {
|
||||
if (originalFeatureTogglesValues != null) {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
valuesField.set(null, originalFeatureTogglesValues)
|
||||
Timber.i("FeatureToggles.values restored")
|
||||
val saved = originalVersionValues ?: return
|
||||
val versionField = getVersionField()
|
||||
|
||||
saved.forEach { (toggle, originalVersion) ->
|
||||
versionField.set(toggle, originalVersion)
|
||||
}
|
||||
|
||||
Timber.i("FeatureToggles.values restored")
|
||||
} catch (e: Exception) {
|
||||
Timber.e("FeatureToggles.values didn't restored with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVersionField(): Field {
|
||||
val field = FeatureToggles::class.java.getDeclaredField("version")
|
||||
field.isAccessible = true
|
||||
return field
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ENABLED_VERSION = "1.0.0"
|
||||
const val DISABLED_VERSION = "undefined"
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.junit.Rule
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class BaseTestCase : TestCase(
|
||||
|
|
@ -68,6 +70,12 @@ abstract class BaseTestCase : TestCase(
|
|||
*/
|
||||
val composeTestRule = createEmptyComposeRule()
|
||||
|
||||
private val semanticTreePrinterRule = object : TestWatcher() {
|
||||
override fun failed(e: Throwable?, description: Description?) {
|
||||
runCatching { printAllRoots() }
|
||||
}
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleChain: TestRule = RuleChain
|
||||
|
|
@ -76,6 +84,7 @@ abstract class BaseTestCase : TestCase(
|
|||
.around(permissionRule)
|
||||
.around(apiEnvironmentRule)
|
||||
.around(composeTestRule)
|
||||
.around(semanticTreePrinterRule)
|
||||
|
||||
/**
|
||||
* Initialization order is important:
|
||||
|
|
@ -140,6 +149,16 @@ abstract class BaseTestCase : TestCase(
|
|||
.printToLog(tag, maxDepth)
|
||||
}
|
||||
|
||||
fun printAllRoots(
|
||||
tag: String = "ComposeTree",
|
||||
) {
|
||||
val roots = composeTestRule.onAllNodes(isRoot())
|
||||
val count = roots.fetchSemanticsNodes().size
|
||||
repeat(count) { index ->
|
||||
roots[index].printToLog("$tag[$index]")
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForIdle() = composeTestRule.waitForIdle()
|
||||
|
||||
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.components.buttons.actions.HasBadgeKey
|
||||
import com.tangem.core.ui.components.buttons.actions.IsDimmedKey
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
fun assertElementDoesNotExist(
|
||||
elementProvider: () -> KNode,
|
||||
|
|
@ -42,4 +46,51 @@ fun Any.assertHasBadge(expectedValue: Boolean = true) {
|
|||
is KNode, is LazyListItemNode -> this.assert(matcher)
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${this::class}")
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertSortedByVolumeDescending() {
|
||||
val volumes = this.mapNotNull { parseVolume(it) }
|
||||
assertFalse("Trading volumes list should not be empty", volumes.isEmpty())
|
||||
assertTrue(
|
||||
"Exchanges list should be sorted by volume in descending order",
|
||||
volumes == volumes.sortedDescending(),
|
||||
)
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertExchangeTypesAreCexOrDex() {
|
||||
assertFalse("Exchange types list should not be empty", isEmpty())
|
||||
forEach { node ->
|
||||
val text = extractText(node)
|
||||
assertTrue(
|
||||
"Exchange type should be 'CEX' or 'DEX', but found: $text",
|
||||
text == "CEX" || text == "DEX",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertTrustScoresValid() {
|
||||
val validScores = setOf("Risky", "Caution", "Trusted")
|
||||
assertFalse("Trust scores list should not be empty", isEmpty())
|
||||
forEach { node ->
|
||||
val text = extractText(node)
|
||||
assertTrue(
|
||||
"Trust score should be one of $validScores, but found: $text",
|
||||
text in validScores,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text value from a semantic node's config.
|
||||
*/
|
||||
private fun extractText(node: SemanticsNode): String? {
|
||||
if (SemanticsProperties.Text in node.config) {
|
||||
return node.config[SemanticsProperties.Text].firstOrNull()?.text
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseVolume(node: SemanticsNode): Double? {
|
||||
val text = extractText(node) ?: return null
|
||||
return text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull()
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.test.ComposeTimeoutException
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
||||
fun KNode.clickWithAssertion() {
|
||||
|
|
@ -34,3 +36,34 @@ fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
|
|||
assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
fun KNode.clickAndWaitFor(
|
||||
rule: ComposeTestRule,
|
||||
timeoutMs: Long = 5_000,
|
||||
maxRetries: Int = 3,
|
||||
expectedCondition: () -> Unit,
|
||||
) {
|
||||
for (attempt in 1..maxRetries) {
|
||||
performClick()
|
||||
rule.waitForIdle()
|
||||
|
||||
try {
|
||||
rule.waitUntil(timeoutMs) {
|
||||
runCatching { expectedCondition() }.isSuccess
|
||||
}
|
||||
return
|
||||
} catch (_: ComposeTimeoutException) {
|
||||
}
|
||||
}
|
||||
|
||||
throw AssertionError("Condition not met after $maxRetries click attempts")
|
||||
}
|
||||
|
||||
fun KNode.performTextInputInChunks(
|
||||
text: String,
|
||||
chunkSize: Int = 2
|
||||
) {
|
||||
text.chunked(chunkSize).forEach { chunk ->
|
||||
performTextInput(chunk)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -46,7 +47,23 @@ fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
|
|||
}
|
||||
|
||||
fun BaseTestCase.openTheAppFromRecents() {
|
||||
device.uiDevice.waitForIdle()
|
||||
|
||||
device.uiDevice.pressRecentApps()
|
||||
device.uiDevice.waitForIdle()
|
||||
|
||||
val recentsOpened = device.uiDevice.wait(
|
||||
Until.hasObject(By.res("com.android.launcher3:id/snapshot")),
|
||||
3_000
|
||||
)
|
||||
|
||||
if (!recentsOpened) {
|
||||
device.uiDevice.pressRecentApps()
|
||||
device.uiDevice.wait(
|
||||
Until.hasObject(By.res("com.android.launcher3:id/snapshot")),
|
||||
3_000
|
||||
)
|
||||
}
|
||||
|
||||
val centerX = device.uiDevice.displayWidth / 2
|
||||
val centerY = device.uiDevice.displayHeight / 3
|
||||
|
|
@ -54,6 +71,14 @@ fun BaseTestCase.openTheAppFromRecents() {
|
|||
device.uiDevice.click(centerX, centerY)
|
||||
}
|
||||
|
||||
fun BaseTestCase.collapseAppByHomeButton() {
|
||||
device.uiDevice.pressHome()
|
||||
device.uiDevice.wait(
|
||||
Until.hasObject(By.pkg(device.uiDevice.launcherPackageName)),
|
||||
3_000
|
||||
)
|
||||
}
|
||||
|
||||
fun BaseTestCase.disableWiFi() {
|
||||
device.uiDevice.executeShellCommand("svc wifi disable")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
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
|
||||
import com.tangem.screens.onMarketsScreen
|
||||
import com.tangem.screens.onMarketsTokenDetailsScreen
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
|
@ -23,4 +27,55 @@ fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName:
|
|||
waitForIdle()
|
||||
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.assertMarketsExchangesScreen() {
|
||||
step("Assert 'Title' is displayed") {
|
||||
onMarketsExchangesScreen { exchangesTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Exchange name' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeName.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Logo' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeLogo.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Exchange type' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeType.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Trust score' is displayed") {
|
||||
onMarketsExchangesScreen { trustScore.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openMarketsScreen() {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Markets' screen") {
|
||||
swipeMarketsBlock(SwipeDirection.UP)
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
||||
openMarketsScreen()
|
||||
if (shouldClickSeeAllButton)
|
||||
step("Click on 'See all' button") {
|
||||
onMarketsScreen { seeAllButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on '$tokenName' token") {
|
||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Scroll down") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click on 'Listed on exchanges' block") {
|
||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import 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 AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.common_add_token))
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasParent
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.features.onramp.impl.R
|
||||
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 MarketsExchangesPageObject(private val provider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<MarketsExchangesPageObject>(semanticsProvider = provider) {
|
||||
|
||||
fun allTradingVolumeNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT_TEXT))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasParent(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
fun allTrustScoreNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
val exchangesTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val exchangeName: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
}
|
||||
|
||||
val exchangeLogo: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_ICON)
|
||||
}
|
||||
|
||||
val tradingVolume: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
|
||||
}
|
||||
|
||||
val exchangeType: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_PRICE)
|
||||
}
|
||||
|
||||
val trustScore: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)
|
||||
}
|
||||
|
||||
val tryAgainButton: KNode = child {
|
||||
hasText(getResourceString(R.string.alert_button_try_again))
|
||||
}
|
||||
|
||||
val errorMessage: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_loading_error_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onMarketsExchangesScreen(function: MarketsExchangesPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -35,6 +35,25 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val listedOnExchangesCount: KNode = child {
|
||||
hasTestTag(MarketsTestTags.LISTED_ON_EXCHANGES_COUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val listedOnBlockContainer: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_token_details_listed_on), substring = true)
|
||||
}
|
||||
|
||||
val listedOnEmptyText: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_token_details_empty_exchanges))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val seeAllButton: KNode = child {
|
||||
hasText(getResourceString(com.tangem.core.ui.R.string.common_see_all))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenWithTitle(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addressesShimmer: KNode = child {
|
||||
hasTestTag(SendAddressScreenTestTags.ADDRESSES_SHIMMER)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val topAppBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(CoreUiR.string.common_address))
|
||||
|
|
@ -62,7 +67,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
}
|
||||
|
||||
val clearTextFieldButton: KNode = child {
|
||||
hasContentDescription(getResourceString(CoreUiR.string.common_close))
|
||||
hasTestTag(SendAddressScreenTestTags.CROSS_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
|
@ -46,6 +47,13 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
}
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun marketsTokenWithTitle(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
hasText(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ 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.SearchBarTestTags
|
||||
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.test.*
|
||||
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
|
||||
|
|
@ -48,6 +45,11 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val searchBarBlock: KNode = child {
|
||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val searchBarIcon: KNode = child {
|
||||
hasTestTag(SearchBarTestTags.ICON)
|
||||
useUnmergedTree = true
|
||||
|
|
@ -58,12 +60,28 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val tryAgainButton: KNode = child {
|
||||
hasText(getResourceString(R.string.alert_button_try_again))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val unableToLoadData: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_loading_error_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenWithName(tokenName: String): KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
hasAnyChild(withText(tokenName))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun marketsTokenWithName(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
hasText(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
|
||||
}
|
||||
|
||||
val swapTokensOnscreenButton: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON)
|
||||
val replaceTokensButton: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.REPLACE_TOKENS_BUTTON)
|
||||
}
|
||||
|
||||
val receiveAmount: KNode = child {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class TopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
) {
|
||||
val moreButton: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.clickAndWaitFor
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
|
|
@ -18,6 +19,7 @@ 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
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -45,6 +47,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
onTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click 'Contact support' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { contactSupportButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
|
|
@ -53,6 +56,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("893")
|
||||
@DisplayName("Send feedback: failed transaction")
|
||||
@Test
|
||||
|
|
@ -94,17 +98,29 @@ class FeedbackTest : BaseTestCase() {
|
|||
step("Enter address") {
|
||||
onSendAddressScreen { addressTextField.performTextInput(recipientAddress) }
|
||||
}
|
||||
step("Click 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
step("Assert text field contains text: $recipientAddress") {
|
||||
onSendAddressScreen { addressTextField.assertTextEquals(recipientAddress) }
|
||||
}
|
||||
step("Assert sеnding text is displayed") {
|
||||
onSendConfirmScreen { sendingText.assertIsDisplayed() }
|
||||
step("Click 'Next' button") {
|
||||
onSendAddressScreen {
|
||||
nextButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendConfirmScreen { sendingText.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Click 'Send' button") {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen {
|
||||
sendButton.assertIsEnabled()
|
||||
sendButton.performClick()
|
||||
sendButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onFailedTransactionDialog { dialogContainer.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Check 'Failed transaction' dialog") {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.tap.domain.sdk.mocks.content.*
|
|||
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
|
||||
|
|
@ -85,6 +86,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("870")
|
||||
@DisplayName("Scan: Card with Ed25519 curve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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
|
||||
|
|
@ -460,6 +461,7 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4396")
|
||||
@DisplayName("Action buttons (main screen): click on buttons without data")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
|
|||
val scenarioName = "user_tokens_api"
|
||||
val scenarioState = "CustomTokenAdded"
|
||||
val tokenTitle = "Myria"
|
||||
val balance = "$3,299.18"
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
|
|
@ -134,8 +135,8 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
step("Assert dash sign is displayed in total balance") {
|
||||
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
|
||||
step("Assert correct balance is displayed in total balance") {
|
||||
onMainScreen { totalBalanceText.assertTextContains(balance) }
|
||||
}
|
||||
step("Assert $tokenTitle is unreachable") {
|
||||
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 4).assertIsUnreachable() }
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
|||
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
||||
}
|
||||
step("Press 'Home' to collapse the app") {
|
||||
device.uiDevice.pressHome()
|
||||
collapseAppByHomeButton()
|
||||
}
|
||||
step("Open the app from recent apps") {
|
||||
openTheAppFromRecents()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
package com.tangem.tests.markets
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.annotations.ApiEnv
|
||||
import com.tangem.common.annotations.ApiEnvConfig
|
||||
import com.tangem.common.extensions.*
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.scenarios.assertMarketsExchangesScreen
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openMarketsExchangesScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMarketsExchangesScreen
|
||||
import com.tangem.screens.onMarketsScreen
|
||||
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 MarketsExchangesTest : BaseTestCase() {
|
||||
|
||||
@Test
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@AllureId("58")
|
||||
@DisplayName("Markets: verify exchanges list screen")
|
||||
fun marketsExchangesListTest() {
|
||||
val tokenName = "Solana"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName = tokenName, shouldClickSeeAllButton = true)
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("56")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchanges block is displayed in token details")
|
||||
fun marketsExchangesBlockDisplayedTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Markets' screen") {
|
||||
swipeMarketsBlock(SwipeDirection.UP)
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on '$tokenName' token") {
|
||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Scroll down") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has title") {
|
||||
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has exchanges count") {
|
||||
onMarketsScreen { listedOnExchangesCount.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has arrow button") {
|
||||
onMarketsScreen { listedOnBlockContainer.assertHasClickAction() }
|
||||
}
|
||||
step("Tap on 'Listed on exchanges' block and navigate to exchanges list") {
|
||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("60")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchanges list is sorted by volume descending")
|
||||
fun marketsExchangesListSortedByVolumeTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert exchanges are sorted by trading volume in descending order") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allTradingVolumeNodes().assertSortedByVolumeDescending() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("61")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchange types are CEX or DEX")
|
||||
fun marketsExchangesTypeTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert exchange types list is not empty and all types are 'CEX' or 'DEX'") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allExchangeTypeNodes().assertExchangeTypesAreCexOrDex() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("62")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchange trust scores are valid")
|
||||
fun marketsExchangesTrustScoreTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert trust scores list is not empty and all scores are valid") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allTrustScoreNodes().assertTrustScoresValid() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("57")
|
||||
@DisplayName("Markets: verify empty exchanges state")
|
||||
fun marketsExchangesEmptyTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val scenarioName = "coins_bitcoin"
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "EmptyExchanges")
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
},
|
||||
).run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block shows no exchanges") {
|
||||
onMarketsScreen { listedOnEmptyText.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("59")
|
||||
@DisplayName("Markets: verify exchanges error state and retry")
|
||||
fun marketsExchangesErrorStateTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val scenarioName = "bitcoin_exchange"
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "Unreachable")
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
},
|
||||
).run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert 'Unable to load data' error state is displayed") {
|
||||
step("Assert 'Error message' is displayed") {
|
||||
onMarketsExchangesScreen { errorMessage.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Try again button' is displayed") {
|
||||
onMarketsExchangesScreen { tryAgainButton.assertIsDisplayed() }
|
||||
}
|
||||
|
||||
}
|
||||
step("Set WireMock scenario '$scenarioName' to state 'Started'") {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "Started")
|
||||
}
|
||||
step("Tap 'Try again' button") {
|
||||
onMarketsExchangesScreen { tryAgainButton.clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed after retry") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_SHORTENE
|
|||
import com.tangem.common.constants.TestConstants.ENS_NAME
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS_WITH_TAG
|
||||
import com.tangem.common.extensions.clickAndWaitFor
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.clearClipboard
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
|
|
@ -115,6 +117,9 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
clearClipboard()
|
||||
}
|
||||
).run {
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, recipientAddress)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
|
|
@ -124,10 +129,11 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Open 'Send Address' screen") {
|
||||
openSendAddressScreen(tokenName, sendAmount)
|
||||
}
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, recipientAddress)
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
waitForIdle()
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert address text field contains correct recipient address") {
|
||||
|
|
@ -142,8 +148,22 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Set clipboard text") {
|
||||
setClipboardText(context, invalidAddress)
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Cross' button") {
|
||||
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
|
||||
waitForIdle()
|
||||
onSendAddressScreen {
|
||||
clearTextFieldButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendAddressScreen { addressPasteButton.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
|
|
@ -154,20 +174,38 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Assert invalid address text field title is displayed") {
|
||||
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
|
||||
}
|
||||
step("Assert 'Next' button is disabled") {
|
||||
onSendAddressScreen { nextButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, walletAddress)
|
||||
}
|
||||
step("Assert 'Next' button is disabled") {
|
||||
onSendAddressScreen { nextButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Cross' button") {
|
||||
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
|
||||
onSendAddressScreen {
|
||||
clearTextFieldButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendAddressScreen { addressPasteButton.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertDoesNotExist() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
waitForIdle()
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert address text field contains invalid address") {
|
||||
onSendAddressScreen { addressTextField.assertTextContains(walletAddress) }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendAddressScreen {
|
||||
addressTextField.assertTextContains(walletAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert 'Address is the same as wallet address' error title is displayed") {
|
||||
onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) }
|
||||
|
|
|
|||
|
|
@ -267,22 +267,21 @@ class SendConfirmScreenTest : BaseTestCase() {
|
|||
step("Click on 'Next' button") {
|
||||
onSendScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Turn off internet") {
|
||||
disableWiFi()
|
||||
disableMobileData()
|
||||
}
|
||||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Turn on internet") {
|
||||
enableWiFi()
|
||||
enableMobileData()
|
||||
}
|
||||
step("Assert 'Network fee info unreachable' warning title is displayed") {
|
||||
onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Check your internet connection' warning message is displayed") {
|
||||
onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() }
|
||||
|
|
@ -290,6 +289,10 @@ class SendConfirmScreenTest : BaseTestCase() {
|
|||
step("Assert warning icon is displayed") {
|
||||
onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Turn on internet") {
|
||||
enableWiFi()
|
||||
enableMobileData()
|
||||
}
|
||||
step("Click on 'Refresh' button") {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ 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.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -32,6 +33,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
|
||||
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4287")
|
||||
@DisplayName("Warnings: check warning, when sending less than reserve")
|
||||
@Test
|
||||
|
|
@ -85,6 +87,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4286")
|
||||
@DisplayName("Warnings: check warning when sending amount equal to reserve")
|
||||
@Test
|
||||
|
|
@ -139,6 +142,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4288")
|
||||
@DisplayName("Warnings: check warning when sending greater than reserve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
package com.tangem.tests.swap
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.R
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.SwapEntryPoint
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openSwapScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
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 SearchAndSwapTest : BaseTestCase() {
|
||||
|
||||
@AllureId("8520")
|
||||
@DisplayName("Search and Swap: add token without derivation")
|
||||
@Test
|
||||
fun addTokenWithoutDerivationTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "Tether"
|
||||
val swapTokenSymbol = "ETH"
|
||||
val receiveTokenSymbol = "USDT"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenName' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8519")
|
||||
@DisplayName("Search and Swap: add token with derivation")
|
||||
@Test
|
||||
fun addTokenWithDerivationTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "TRON"
|
||||
val swapTokenSymbol = "ETH"
|
||||
val receiveTokenSymbol = "TRX"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenSymbol' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenSymbol) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8523")
|
||||
@DisplayName("Search and Swap: Markets error")
|
||||
@Test
|
||||
fun marketsErrorTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val scenarioName = "coins_list_api"
|
||||
val scenarioState = "Error"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(scenarioName, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Unable to load data...' error is displayed") {
|
||||
onSwapSelectTokenScreen { unableToLoadData.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Try again' button is displayed") {
|
||||
onSwapSelectTokenScreen { tryAgainButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8522")
|
||||
@DisplayName("Search and Swap: check 'Unsupported token pair' warning")
|
||||
@Test
|
||||
fun unsupportedTokenPairTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "Pepe"
|
||||
val warningTitle = getResourceString(R.string.warning_express_unsupported_pair_title)
|
||||
val warningMessage = getResourceString(R.string.warning_express_unsupported_pair_description)
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenName' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert warning title '$warningTitle' is displayed") {
|
||||
onSwapTokenScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning message '$warningMessage' is displayed") {
|
||||
onSwapTokenScreen { warningMessage(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning icon is displayed'") {
|
||||
onSwapTokenScreen { warningIcon(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8521")
|
||||
@DisplayName("Swap: search token on Swap token screen")
|
||||
@Test
|
||||
fun networkFeeTest() {
|
||||
val tokenTitle = "Ethereum"
|
||||
val swapTokenSymbol = "TRX"
|
||||
val receiveTokenSymbol = "ETH"
|
||||
val swapTokenName = "TRON"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
||||
}
|
||||
step("Click on 'Replace tokens' button") {
|
||||
onSwapTokenScreen { replaceTokensButton.performClick() }
|
||||
}
|
||||
step("Click on 'Select token' icon") {
|
||||
onSwapTokenScreen { selectTokenIcon.performClick() }
|
||||
}
|
||||
step("Click on 'Search' icon") {
|
||||
onSwapChooseTokenScreen { searchIcon.performClick() }
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapChooseTokenScreen { searchTextField.performClick() }
|
||||
}
|
||||
step("Type '$swapTokenSymbol' in 'Search' text field") {
|
||||
onSwapChooseTokenScreen { searchTextField.performTextInputInChunks(swapTokenSymbol) }
|
||||
}
|
||||
step("Click on token with name: '$swapTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapChooseTokenScreen { marketsTokenWithTitle(swapTokenName).performClick() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.swap
|
|||
import androidx.compose.ui.test.longClick
|
||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.extensions.assertHasBadge
|
||||
import com.tangem.common.extensions.restartApp
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
|
|
@ -208,7 +209,10 @@ class SwapStoriesTest : BaseTestCase() {
|
|||
}
|
||||
step("Assert 'Swap' button has badge") {
|
||||
waitForIdle()
|
||||
onMainScreen { swapButton.assertHasBadge() }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
composeTestRule.mainClock.advanceTimeBy(500)
|
||||
onMainScreen { swapButton.assertHasBadge() }
|
||||
}
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +307,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +397,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -442,7 +442,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Swap tokens on screen' button") {
|
||||
onSwapTokenScreen { swapTokensOnscreenButton.performClick() }
|
||||
onSwapTokenScreen { replaceTokensButton.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
|
|
@ -557,9 +557,6 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiEnv(
|
||||
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
|
||||
)
|
||||
@AllureId("8536")
|
||||
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -139,8 +138,6 @@ interface ApplicationEntryPoint {
|
|||
|
||||
fun getApiConfigsManager(): ApiConfigsManager
|
||||
|
||||
fun getUserTokensResponseStore(): UserTokensResponseStore
|
||||
|
||||
fun getUserWalletsListRepository(): UserWalletsListRepository
|
||||
|
||||
fun getTangemHotSdk(): TangemHotSdk
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ 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.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
|
|
@ -219,9 +218,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val apiConfigsManager: ApiConfigsManager
|
||||
get() = entryPoint.getApiConfigsManager()
|
||||
|
||||
private val userTokensResponseStore: UserTokensResponseStore
|
||||
get() = entryPoint.getUserTokensResponseStore()
|
||||
|
||||
private val userWalletsListRepository
|
||||
get() = entryPoint.getUserWalletsListRepository()
|
||||
|
||||
|
|
@ -385,7 +381,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
settingsManager = settingsManager,
|
||||
uiMessageSender = uiMessageSender,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
trackingContextProxy = trackingContextProxy,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -8,5 +9,5 @@ class CustomerIoFeatureToggles @Inject constructor(
|
|||
) {
|
||||
|
||||
val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "CUSTOMER_IO_ENABLED")
|
||||
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.CUSTOMER_IO_ENABLED)
|
||||
}
|
||||
|
|
@ -2,11 +2,9 @@ package com.tangem.tap.common.analytics.appsflyer
|
|||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -20,10 +18,9 @@ import kotlin.contracts.contract
|
|||
class AppsFlyerReferralParamsHandler @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob())
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun handle(deepLink: DeepLink) {
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
|
|||
import com.tangem.core.analytics.api.EventLogger
|
||||
import com.tangem.core.analytics.api.UserIdHolder
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener
|
||||
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -26,15 +24,13 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
@ApplicationContext private val context: Context,
|
||||
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
|
||||
tangemAFConversionListener: TangemAFConversionListener,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) : AppsFlyerAnalyticsClient {
|
||||
|
||||
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
|
||||
private val eventConverter = UnderscoreAnalyticsEventConverter()
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
init {
|
||||
with(appsFlyerLib) {
|
||||
setAppId(context.packageName)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
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.orhanobut.logger.Logger
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
import com.orhanobut.logger.Logger as PrettyLogger
|
||||
|
||||
/**
|
||||
* Tangem app logger
|
||||
|
|
@ -21,32 +27,102 @@ class TangemAppLoggerInitializer(
|
|||
/** Initialize */
|
||||
fun initialize() {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
}
|
||||
|
||||
Timber.plant(tree = createTimberTree())
|
||||
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
|
||||
}
|
||||
|
||||
private fun createTimberTree(): Timber.Tree {
|
||||
return object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.log(priority, tag, message, t)
|
||||
}
|
||||
|
||||
if (PERMITTED_PRIORITY.contains(priority)) {
|
||||
appLogsStore.saveLogMessage(
|
||||
tag = tag ?: "TangemAppLogger",
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
finalLogOutput(priority = priority, tag = tag, message = message, t = t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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 [Timber.DebugTree.tag]
|
||||
*/
|
||||
@Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause")
|
||||
Throwable().stackTrace
|
||||
.first { it.className !in fqcnIgnore }
|
||||
.let(::createStackElementTag)
|
||||
}
|
||||
|
||||
finalLogOutput(priority, finalTag, message, throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* copy from [Timber.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 [Timber.DebugTree.Companion]
|
||||
*/
|
||||
private const val MAX_TAG_LENGTH = 23
|
||||
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.tap.core
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class DefaultAppCoroutineScope @Inject constructor(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : AppCoroutineScope {
|
||||
|
||||
private val tag = "AppCoroutineScope"
|
||||
|
||||
override val coroutineContext: CoroutineContext = SupervisorJob() +
|
||||
// keep IO dispatcher to avoid blocking Default with IO operations
|
||||
dispatchers.io +
|
||||
CoroutineName(tag) +
|
||||
CoroutineExceptionHandler { context, throwable ->
|
||||
val coroutineName = context[CoroutineName]?.name.orEmpty()
|
||||
logError(throwable, coroutineName)
|
||||
}
|
||||
|
||||
private fun logError(throwable: Throwable, coroutineName: String) {
|
||||
Logger.withTag(tag).e(
|
||||
messageString = "CoroutineName $coroutineName",
|
||||
throwable = throwable,
|
||||
)
|
||||
val event = ExceptionAnalyticsEvent(
|
||||
exception = throwable,
|
||||
params = mapOf(
|
||||
"source" to tag,
|
||||
"coroutineName" to coroutineName,
|
||||
),
|
||||
)
|
||||
analyticsExceptionHandler.sendException(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.tap.core.ui
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultDesignFeatureToggles @Inject constructor(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : DesignFeatureToggles {
|
||||
override val isRedesignEnabled: Boolean = featureTogglesManager.isFeatureEnabled("APP_REDESIGN_ENABLED")
|
||||
|
||||
override val isRedesignEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.APP_REDESIGN_ENABLED)
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
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(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : HoldToConfirmButtonFeatureToggles {
|
||||
override val isHoldToConfirmEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
"HOLD_TO_CONFIRM_BUTTON_ENABLED",
|
||||
)
|
||||
override val isHoldToConfirmEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.HOLD_TO_CONFIRM_BUTTON_ENABLED)
|
||||
}
|
||||
|
|
@ -231,12 +231,22 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Boolean) {
|
||||
appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
value = eligibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
|
||||
override suspend fun getTangemPayEligibility(): Set<String> {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
default = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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
|
||||
|
|
@ -21,8 +22,6 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -62,8 +61,8 @@ internal object ActivityModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
@DelayedWork
|
||||
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
|
||||
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
fun provideActivityDelayedWorkCoroutineScope(appScope: AppCoroutineScope): CoroutineScope {
|
||||
return appScope
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.domain.card.BuildConfig
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
|
|
@ -17,7 +12,6 @@ import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
|
|||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -38,13 +32,8 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
@GlobalUiMessageSender uiMessageSender: UiMessageSender,
|
||||
appFinisher: AppFinisher,
|
||||
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -56,13 +45,8 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
uiMessageSender = uiMessageSender,
|
||||
appFinisher = appFinisher,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
dispatchers = dispatchers,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
|
@ -30,6 +31,7 @@ internal object UiDependenciesModule {
|
|||
override val vibratorHapticManager = vibratorHapticManager
|
||||
override val appThemeModeHolder = appThemeModeHolder
|
||||
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
|
||||
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
|
||||
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
|
||||
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.LinkHandler
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.finisher.AndroidAppFinisher
|
||||
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 dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -18,21 +23,32 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object UtilsModule {
|
||||
internal interface UtilsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShareManager(): ShareManager = IntentShareManager()
|
||||
@Binds
|
||||
fun provideAppScope(defaultAppScope: DefaultAppCoroutineScope): AppCoroutineScope
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShareManager(): ShareManager = IntentShareManager()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager =
|
||||
IntentSettingsManager(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLinkHandler(appRouter: AppRouter): LinkHandler = LinkHandler(appRouter)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,14 +40,6 @@ internal object ManageTokensDomainModule {
|
|||
return FindTokenUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckIsCurrencyNotAddedUseCase(
|
||||
customTokensRepository: CustomTokensRepository,
|
||||
): CheckIsCurrencyNotAddedUseCase {
|
||||
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSupportedNetworksUseCase(
|
||||
|
|
@ -64,12 +56,6 @@ internal object ManageTokensDomainModule {
|
|||
return ValidateDerivationPathUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase {
|
||||
return CheckHasLinkedTokensUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.nft.utils.NFTCleaner
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -63,9 +62,7 @@ internal object NFTDomainModule {
|
|||
fun providesGetNFTAvailableNetworksUseCase(
|
||||
nftRepository: NFTRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
nftRepository = nftRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.promo.*
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.ShouldShowPromoTokenUseCase
|
||||
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
|
||||
import com.tangem.domain.promo.ShouldShowStoriesUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -17,8 +22,13 @@ internal object PromoDomainModule {
|
|||
fun provideShouldShowSwapPromoWalletUseCase(
|
||||
promoRepository: PromoRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
|
||||
): ShouldShowPromoWalletUseCase {
|
||||
return ShouldShowPromoWalletUseCase(promoRepository, settingsRepository)
|
||||
return ShouldShowPromoWalletUseCase(
|
||||
promoRepository,
|
||||
settingsRepository,
|
||||
newPromoBannersFeatureToggles.isNewPromoBannersEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,4 +34,18 @@ internal object QrScanningDomainModule {
|
|||
fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase {
|
||||
return ParseQrCodeUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideResolveQrSendTargetsUseCase(
|
||||
multiAccountListSupplier: MultiAccountListSupplier,
|
||||
qrScanningEventsRepository: QrScanningEventsRepository,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
): ResolveQrSendTargetsUseCase {
|
||||
return ResolveQrSendTargetsUseCase(
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
qrScanningEventsRepository = qrScanningEventsRepository,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase
|
||||
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
|
||||
import com.tangem.domain.tokensync.usecase.SyncTokensUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokenSyncDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideObserveTokenSyncUseCase(tokenSyncRepository: TokenSyncRepository): ObserveTokenSyncUseCase {
|
||||
return ObserveTokenSyncUseCase(
|
||||
tokenSyncRepository = tokenSyncRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAcknowledgeTokenSyncCompletionUseCase(
|
||||
tokenSyncRepository: TokenSyncRepository,
|
||||
): AcknowledgeTokenSyncCompletionUseCase {
|
||||
return AcknowledgeTokenSyncCompletionUseCase(
|
||||
tokenSyncRepository = tokenSyncRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncTokensUseCase(
|
||||
tokenSyncRepository: TokenSyncRepository,
|
||||
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
): SyncTokensUseCase {
|
||||
return SyncTokensUseCase(
|
||||
tokenSyncRepository = tokenSyncRepository,
|
||||
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,22 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
|
@ -36,7 +33,6 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
internal object TokensDomainModule {
|
||||
|
||||
@Provides
|
||||
|
|
@ -53,67 +49,6 @@ internal object TokensDomainModule {
|
|||
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyUseCase(
|
||||
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetSingleCryptoCurrencyStatusUseCase {
|
||||
return GetSingleCryptoCurrencyStatusUseCase(
|
||||
currencyStatusOperations = baseCurrencyStatusOperations,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyWarningsUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): GetCurrencyWarningsUseCase {
|
||||
return GetCurrencyWarningsUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
dispatchers = dispatchers,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
currencyStatusOperations = baseCurrencyStatusOperations,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchCurrencyStatusUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchCurrencyStatusUseCase {
|
||||
return FetchCurrencyStatusUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrencyUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): GetCryptoCurrencyUseCase {
|
||||
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
|
|
@ -132,30 +67,6 @@ internal object TokensDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyStatusByNetworkUseCase(
|
||||
currencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetNetworkCoinStatusUseCase {
|
||||
return GetNetworkCoinStatusUseCase(
|
||||
currencyStatusOperations = currencyStatusOperations,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
currencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
): GetFeePaidCryptoCurrencyStatusSyncUseCase {
|
||||
return GetFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
currencyStatusOperations = currencyStatusOperations,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetMinimumTransactionAmountSyncUseCase(
|
||||
|
|
@ -168,11 +79,11 @@ internal object TokensDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsCryptoCurrencyCoinCouldHideUseCase(
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): IsCryptoCurrencyCoinCouldHideUseCase {
|
||||
return IsCryptoCurrencyCoinCouldHideUseCase(
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
fun provideIsCryptoCurrencyCouldHideUseCase(
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): IsCryptoCurrencyCouldHideUseCase {
|
||||
return IsCryptoCurrencyCouldHideUseCase(
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -239,37 +150,13 @@ internal object TokensDomainModule {
|
|||
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBaseCurrencyStatusOperations(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
|
||||
multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): BaseCurrencyStatusOperations {
|
||||
return BaseCurrencyStatusOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleStakingBalanceSupplier = singleStakingBalanceSupplier,
|
||||
multiStakingBalanceSupplier = multiStakingBalanceSupplier,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletBalanceFetcher(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesFetcher: MultiWalletCryptoCurrenciesFetcher,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
expressServiceFetcher: ExpressServiceFetcher,
|
||||
multiWalletAccountListFetcher: MultiWalletAccountListFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
|
|
@ -279,8 +166,10 @@ internal object TokensDomainModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletBalanceFetcher {
|
||||
return WalletBalanceFetcher(
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiWalletCryptoCurrenciesFetcher = multiWalletCryptoCurrenciesFetcher,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
multiWalletAccountListFetcher = multiWalletAccountListFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.data.wallets.hot.TangemHotWalletSigner
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
|
|
@ -19,13 +19,10 @@ import com.tangem.domain.transaction.WalletAddressServiceRepository
|
|||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.transaction.usecase.gasless.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
|
|
@ -56,8 +53,8 @@ internal object TransactionDomainModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
pushNotificationsRepository: PushNotificationsRepository,
|
||||
appScope: AppCoroutineScope,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
demoConfig = DemoConfig,
|
||||
|
|
@ -65,7 +62,7 @@ internal object TransactionDomainModule {
|
|||
transactionRepository = transactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.io),
|
||||
parallelUpdatingScope = appScope,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
pushNotificationsRepository = pushNotificationsRepository,
|
||||
)
|
||||
|
|
@ -279,14 +276,12 @@ internal object TransactionDomainModule {
|
|||
@Singleton
|
||||
fun provideGetAvailableFeeTokensUseCase(
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetAvailableFeeTokensUseCase {
|
||||
return GetAvailableFeeTokensUseCase(
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -296,17 +291,15 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForGaslessUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getFeeUseCase: GetFeeUseCase,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetFeeForGaslessUseCase {
|
||||
return GetFeeForGaslessUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
|
|
@ -317,16 +310,14 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetFeeForTokenUseCase {
|
||||
return GetFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -344,13 +335,13 @@ internal object TransactionDomainModule {
|
|||
fun provideCreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
getSingCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
): CreateAndSendGaslessTransactionUseCase {
|
||||
return CreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
getSingleCryptoCurrencyStatusUseCase = getSingCryptoCurrencyStatusUseCase,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
|
|
@ -362,16 +353,14 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForTokenUseCase {
|
||||
return EstimateFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -381,8 +370,7 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForGaslessTxUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
estimateFeeUseCase: EstimateFeeUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForGaslessTxUseCase {
|
||||
|
|
@ -390,8 +378,7 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
estimateFeeUseCase = estimateFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.error.FeeErrorResolver
|
||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
|
|
@ -15,8 +16,6 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -145,12 +144,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyMinAmountUseCase(
|
||||
feeRepository: FeeRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyMinAmountUseCase {
|
||||
return YieldSupplyMinAmountUseCase(
|
||||
feeRepository = feeRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +158,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyGetCurrentFeeUseCase(
|
||||
feeRepository: FeeRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyGetCurrentFeeUseCase {
|
||||
return YieldSupplyGetCurrentFeeUseCase(
|
||||
feeRepository = feeRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -173,12 +172,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyGetMaxFeeUseCase(
|
||||
yieldSupplyRepository: YieldSupplyRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyGetMaxFeeUseCase {
|
||||
return YieldSupplyGetMaxFeeUseCase(
|
||||
yieldSupplyRepository = yieldSupplyRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -253,12 +252,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyPendingProcessorUseCase(
|
||||
yieldSupplyRepository: YieldSupplyRepository,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
dispatcherProvider: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): YieldSupplyPendingTracker {
|
||||
return YieldSupplyPendingTracker(
|
||||
yieldSupplyRepository = yieldSupplyRepository,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.io),
|
||||
coroutineScope = appScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.domain.scanCard
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
/**
|
||||
|
|
@ -12,5 +13,5 @@ class CardScanningFeatureToggles internal constructor(
|
|||
) {
|
||||
|
||||
val isNewCardScanningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.NEW_CARD_SCANNING_ENABLED)
|
||||
}
|
||||
|
|
@ -6,11 +6,9 @@ import androidx.annotation.StringRes
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.Log
|
||||
import com.tangem.Message
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.common.*
|
||||
import com.tangem.common.authentication.AuthenticationManager
|
||||
import com.tangem.common.authentication.keystore.KeystoreManager
|
||||
import com.tangem.common.core.*
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
|
|
@ -18,24 +16,14 @@ import com.tangem.common.extensions.hexToBytes
|
|||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.crypto.bip39.DefaultMnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -65,11 +53,11 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
|||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
|
||||
|
|
@ -80,17 +68,10 @@ internal class DefaultTangemSdkManager(
|
|||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val appFinisher: AppFinisher,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
||||
private val tangemSdk: TangemSdk
|
||||
get() = cardSdkConfigRepository.sdk
|
||||
|
||||
|
|
@ -101,10 +82,32 @@ internal class DefaultTangemSdkManager(
|
|||
)
|
||||
}
|
||||
override val needEnrollBiometrics: Boolean
|
||||
get() = tangemSdk.authenticationManager.needEnrollBiometrics
|
||||
get() {
|
||||
val isNeedEnrollBiometrics = tangemSdk.authenticationManager.needEnrollBiometrics
|
||||
if (isNeedEnrollBiometrics) {
|
||||
analyticsErrorHandler.sendErrorEvent(
|
||||
AnalyticsEvent(
|
||||
category = "TangemSdkManager",
|
||||
event = "needEnrollBiometrics",
|
||||
),
|
||||
)
|
||||
}
|
||||
return isNeedEnrollBiometrics
|
||||
}
|
||||
|
||||
override val canUseBiometry: Boolean
|
||||
get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
|
||||
get() {
|
||||
val isCanUseBiometry = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
|
||||
if (!isCanUseBiometry) {
|
||||
analyticsErrorHandler.sendErrorEvent(
|
||||
AnalyticsEvent(
|
||||
category = "TangemSdkManager",
|
||||
event = "cantUseBiometry",
|
||||
),
|
||||
)
|
||||
}
|
||||
return isCanUseBiometry
|
||||
}
|
||||
|
||||
override val keystoreManager: KeystoreManager
|
||||
get() = tangemSdk.keystoreManager
|
||||
|
|
@ -115,52 +118,12 @@ internal class DefaultTangemSdkManager(
|
|||
override val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io)
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
needEnrollBiometrics
|
||||
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
|
||||
Log.error {
|
||||
"Trying to access `needEnrollBiometrics` flag when authentication manager is not initialized: " +
|
||||
if (awaitInitialization) "awaiting initialization" else "failing"
|
||||
}
|
||||
|
||||
if (awaitInitialization) {
|
||||
val manager = awaitAuthenticationManagerInitialization()
|
||||
|
||||
if (manager.isInitialized) {
|
||||
manager.needEnrollBiometrics
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return needEnrollBiometrics
|
||||
}
|
||||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
canUseBiometry
|
||||
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
|
||||
Log.error {
|
||||
"Trying to access `canUseBiometry` flag when authentication manager is not initialized: " +
|
||||
if (awaitInitialization) "awaiting initialization" else "failing"
|
||||
}
|
||||
|
||||
if (awaitInitialization) {
|
||||
val manager = awaitAuthenticationManagerInitialization()
|
||||
|
||||
if (manager.isInitialized) {
|
||||
manager.canAuthenticate || manager.needEnrollBiometrics
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return canUseBiometry
|
||||
}
|
||||
|
||||
override suspend fun scanProduct(
|
||||
|
|
@ -434,59 +397,6 @@ internal class DefaultTangemSdkManager(
|
|||
tangemSdk.config.userCodeRequestPolicy = policy
|
||||
}
|
||||
|
||||
private suspend fun awaitAuthenticationManagerInitialization(): AuthenticationManager {
|
||||
return awaitInitializationMutex.withLock {
|
||||
var attemps = 0
|
||||
|
||||
do {
|
||||
if (tangemSdk.authenticationManager.isInitialized) {
|
||||
break
|
||||
} else {
|
||||
if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) {
|
||||
analyticsExceptionHandler.sendException(
|
||||
ExceptionAnalyticsEvent(
|
||||
exception = IllegalStateException(
|
||||
"Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts",
|
||||
),
|
||||
),
|
||||
)
|
||||
showAlert()
|
||||
break
|
||||
} else {
|
||||
delay(timeMillis = 400)
|
||||
}
|
||||
}
|
||||
} while (true)
|
||||
|
||||
tangemSdk.authenticationManager
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAlert() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(id = R.string.alert_authentication_error_message),
|
||||
title = resourceReference(id = R.string.alert_authentication_error_title),
|
||||
isDismissable = false,
|
||||
dismissOnFirstAction = false,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.alert_button_request_support),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
analyticsEventHandler.send(
|
||||
Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.SignIn),
|
||||
)
|
||||
sendFeedbackEmailUseCase(FeedbackEmailType.BiometricsAuthenticationFailed)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = { cancelAction(onClick = appFinisher::finish) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region Twin-specific
|
||||
|
||||
override suspend fun createFirstTwinWallet(
|
||||
|
|
@ -626,8 +536,4 @@ internal class DefaultTangemSdkManager(
|
|||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val MAX_INITIALIZE_ATTEMPTS = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
|
||||
|
|
@ -8,5 +9,5 @@ internal class DefaultTokensFeatureToggles(
|
|||
) : TokensFeatureToggles {
|
||||
|
||||
override val isMultiAddressUtxoEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED")
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.MULTI_ADDRESS_UTXO_ENABLED)
|
||||
}
|
||||
|
|
@ -10,10 +10,10 @@ import com.tangem.core.decompose.di.GlobalUiMessageSender
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
import com.tangem.core.ui.components.bottomsheets.message.*
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
|
|
@ -216,30 +216,28 @@ internal class MainViewModel @Inject constructor(
|
|||
if (!settings.isUpdateFromToast) {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(false)
|
||||
|
||||
val message = BottomSheetMessage.invoke(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
onDismissRequest = ::onBottomSheetDismissed,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = false)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = true)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
val message = bottomSheetMessage {
|
||||
infoBlock {
|
||||
title = resourceReference(R.string.balance_hidden_title)
|
||||
body = resourceReference(R.string.balance_hidden_description)
|
||||
icon(R.drawable.ic_eye_off_outline_24)
|
||||
}
|
||||
onDismiss { onBottomSheetDismissed() }
|
||||
primaryButton {
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button)
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = false)
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button)
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = true)
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.tangem.data.card.TransactionSignerFactory
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
|
|
@ -71,7 +70,6 @@ data class DaggerGraphState(
|
|||
val uiMessageSender: UiMessageSender? = null,
|
||||
val cardArworksProvider: CardArtworksProvider? = null,
|
||||
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
|
||||
val userTokensResponseStore: UserTokensResponseStore? = null,
|
||||
val userWalletsListRepository: UserWalletsListRepository? = null,
|
||||
val tangemHotSdk: TangemHotSdk? = null,
|
||||
val trackingContextProxy: TrackingContextProxy? = null,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -28,9 +29,12 @@ import com.arkivanov.essenty.backhandler.BackHandler
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.LocalRootBackgroundColor
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.LocalTopSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.security.ProvideSecureFlagController
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
|
|
@ -93,6 +97,16 @@ internal fun RootContent(
|
|||
.padding(all = 16.dp),
|
||||
hostState = snackbarHostState,
|
||||
)
|
||||
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemTopSnackbarHost(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.statusBarsPadding()
|
||||
.padding(all = 16.dp),
|
||||
hostState = LocalTopSnackbarHostState.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMessageEffect()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
|||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
import com.tangem.features.nft.component.NFTComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||
import com.tangem.features.onramp.component.*
|
||||
|
|
@ -64,7 +63,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
|
||||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
|
||||
private val onrampComponentFactory: OnrampComponent.Factory,
|
||||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
||||
|
|
@ -140,6 +138,7 @@ internal class ChildFactory @Inject constructor(
|
|||
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
|
||||
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
|
||||
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
|
||||
AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER
|
||||
}
|
||||
|
||||
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None
|
||||
|
|
@ -327,6 +326,7 @@ internal class ChildFactory @Inject constructor(
|
|||
amount = route.amount,
|
||||
tag = route.tag,
|
||||
destinationAddress = route.destinationAddress,
|
||||
entryType = route.entryType.toSendEntryType(),
|
||||
),
|
||||
componentFactory = sendComponentFactoryV2,
|
||||
)
|
||||
|
|
@ -349,6 +349,7 @@ internal class ChildFactory @Inject constructor(
|
|||
val source = when (route.source) {
|
||||
is AppRoute.QrScanning.Source.Send -> SourceType.SEND
|
||||
is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT
|
||||
is AppRoute.QrScanning.Source.MainScreen -> SourceType.MAIN_SCREEN
|
||||
}
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -457,8 +458,8 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.Markets -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = Unit,
|
||||
componentFactory = marketsTokenListComponentFactory,
|
||||
params = FeedEntryRoute.MarketTokenList,
|
||||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Usedesk -> { // TODO [REDACTED_TASK_KEY] pass params
|
||||
|
|
@ -687,4 +688,9 @@ internal class ChildFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AppRoute.Send.EntryType.toSendEntryType(): SendComponent.EntryType = when (this) {
|
||||
AppRoute.Send.EntryType.Manual -> SendComponent.EntryType.Manual
|
||||
AppRoute.Send.EntryType.QR -> SendComponent.EntryType.QR
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import com.tangem.common.routing.DeepLinkRoute
|
|||
import com.tangem.common.routing.DeepLinkScheme
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
|||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import arrow.core.right
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -30,7 +30,7 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
}
|
||||
private val handler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import android.net.Uri
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||
|
|
|
|||
|
|
@ -68,13 +68,20 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
val entryType: EntryType = EntryType.Manual,
|
||||
) : AppRoute(
|
||||
path = "/send/${userWalletId.stringValue}/${currency.id.value}?" +
|
||||
"&$transactionId" +
|
||||
"&$amount" +
|
||||
"&$tag" +
|
||||
"&$destinationAddress",
|
||||
)
|
||||
) {
|
||||
@Serializable
|
||||
enum class EntryType {
|
||||
Manual,
|
||||
QR,
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Details(
|
||||
|
|
@ -139,6 +146,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
STORIES,
|
||||
SETTINGS,
|
||||
ACCOUNT,
|
||||
TOKEN_SYNC_BANNER,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,11 +175,14 @@ sealed class AppRoute(val path: String) : Route {
|
|||
get() = when (this) {
|
||||
is Send -> "/$networkName"
|
||||
WalletConnect -> ""
|
||||
MainScreen -> ""
|
||||
}
|
||||
|
||||
data class Send(val networkName: String) : Source()
|
||||
|
||||
data object WalletConnect : Source()
|
||||
|
||||
data object MainScreen : Source()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +327,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data class Stories(
|
||||
val storyId: String,
|
||||
val nextScreen: AppRoute,
|
||||
val nextScreen: AppRoute? = null,
|
||||
val screenSource: String,
|
||||
) : AppRoute(path = "/stories$storyId")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.common.routing
|
||||
|
||||
import android.net.Uri
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Routes in-app content links (deep links and external URLs)
|
||||
* - `tangem://` scheme → parsed to AppRoute and pushed via AppRouter
|
||||
* - `https://` / `http://` → opened in external browser via UrlOpener
|
||||
* - Unknown scheme → logged and ignored
|
||||
*/
|
||||
class LinkHandler(
|
||||
private val appRouter: AppRouter,
|
||||
) {
|
||||
|
||||
fun navigate(link: String) {
|
||||
val uri = Uri.parse(link)
|
||||
handleTangemDeepLink(uri)
|
||||
}
|
||||
|
||||
private fun handleTangemDeepLink(uri: Uri) {
|
||||
val route = parseDeepLinkToRoute(uri)
|
||||
if (route != null) {
|
||||
appRouter.push(route)
|
||||
} else {
|
||||
Timber.w("ContentLinkHandler: unrecognized tangem deep link: %s", uri)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter", "FunctionOnlyReturningConstant")
|
||||
private fun parseDeepLinkToRoute(uri: Uri): AppRoute? {
|
||||
// TODO [REDACTED_TASK_KEY] refactor deepling routing
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.common.test
|
||||
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TestAppCoroutineScope(override val coroutineContext: CoroutineContext = Dispatchers.Unconfined) : AppCoroutineScope {
|
||||
|
||||
constructor(testScope: TestScope) : this(testScope.coroutineContext)
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
package com.tangem.common.ui.markets
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.common.ui.tokens.TokenPriceText
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.badge.*
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun MarketsListItemV2(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketListItemContentV2(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
.clickable(onClick = onClick)
|
||||
.testTag(MarketsTestTags.TOKENS_LIST_ITEM),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarketListItemContentV2(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
TangemRowContainer(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
TangemIcon(
|
||||
tangemIconUM = TangemIconUM.Url(model.iconUrl, fallbackRes = R.drawable.ic_custom_token_44),
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.layoutId(layoutId = TangemRowLayoutId.HEAD),
|
||||
)
|
||||
|
||||
TokenTitle(
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
|
||||
.padding(horizontal = TangemTheme.dimens2.x2),
|
||||
name = model.name,
|
||||
currencySymbol = model.currencySymbol,
|
||||
)
|
||||
|
||||
TokenPriceText(
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
||||
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
||||
price = model.price.text,
|
||||
priceChangeType = model.price.changeType,
|
||||
)
|
||||
|
||||
TokenSubtitle(
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(end = TangemTheme.dimens2.x2, start = TangemTheme.dimens2.x3),
|
||||
ratingPosition = model.ratingPosition,
|
||||
marketCap = model.marketCap,
|
||||
stakingRate = model.stakingRate,
|
||||
)
|
||||
|
||||
PriceChangeInPercent(
|
||||
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
|
||||
textStyle = TangemTheme.typography2.captionRegular12,
|
||||
type = model.trendType,
|
||||
valueInPercent = model.trendPercentText,
|
||||
)
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Chart(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens2.x2)
|
||||
.layoutId(layoutId = TangemRowLayoutId.TAIL),
|
||||
chartType = model.chartType,
|
||||
chartRawData = model.chartData,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = name,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = currencySymbol,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSubtitle(
|
||||
ratingPosition: String?,
|
||||
marketCap: String?,
|
||||
stakingRate: TextReference?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val ratingColor = mapRatingToColor(ratingPosition)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
TokenRatingPlace(
|
||||
ratingPosition = ratingPosition,
|
||||
ratingColor = ratingColor,
|
||||
)
|
||||
if (marketCap != null) {
|
||||
TokenMarketCapText(
|
||||
ratingColor = ratingColor,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
text = marketCap,
|
||||
)
|
||||
}
|
||||
if (stakingRate != null) {
|
||||
StakingRate(stakingRate = stakingRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenRatingPlace(ratingPosition: String?, ratingColor: Color) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens2.x4),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(height = TangemTheme.dimens2.x4, width = TangemTheme.dimens2.x2),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_laurel_left),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = ratingPosition ?: MINUS,
|
||||
color = ratingColor,
|
||||
style = TangemTheme.typography2.captionSemibold12.copy(letterSpacing = 0.sp),
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Icon(
|
||||
modifier = Modifier.size(height = TangemTheme.dimens2.x4, width = TangemTheme.dimens2.x2),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_laurel_right),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenMarketCapText(text: String, ratingColor: Color, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.alignByBaseline(),
|
||||
text = text,
|
||||
color = ratingColor,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingRate(stakingRate: TextReference, modifier: Modifier = Modifier) {
|
||||
TangemBadge(
|
||||
modifier = modifier.alignByBaseline(),
|
||||
text = stakingRate,
|
||||
shape = TangemBadgeShape.Rounded,
|
||||
size = TangemBadgeSize.X4,
|
||||
type = TangemBadgeType.Tinted,
|
||||
color = TangemBadgeColor.Gray,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.size(height = TangemTheme.dimens2.x6, width = TangemTheme.dimens2.x12),
|
||||
) {
|
||||
if (chartRawData != null) {
|
||||
MarketChartMini(
|
||||
rawData = chartRawData,
|
||||
type = chartType,
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens2.x3)
|
||||
.align(Alignment.Center),
|
||||
radius = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun mapRatingToColor(rating: String?): Color {
|
||||
val isDarkTheme = LocalIsInDarkTheme.current
|
||||
|
||||
return when (rating) {
|
||||
"1" -> if (isDarkTheme) Color(GOLD_PLACE_COLOR_NIGHT) else Color(GOLD_PLACE_COLOR_LIGHT)
|
||||
"2" -> if (isDarkTheme) Color(SILVER_PLACE_COLOR_NIGHT) else Color(SILVER_PLACE_COLOR_LIGHT)
|
||||
"3" -> if (isDarkTheme) Color(BRONZE_PLACE_COLOR_NIGHT) else Color(BRONZE_PLACE_COLOR_LIGHT)
|
||||
else -> TangemTheme.colors2.text.neutral.secondary
|
||||
}
|
||||
}
|
||||
|
||||
private const val GOLD_PLACE_COLOR_NIGHT = 0xFFFBEE76
|
||||
private const val GOLD_PLACE_COLOR_LIGHT = 0xFFD9B900
|
||||
private const val SILVER_PLACE_COLOR_NIGHT = 0xFFAABEF7
|
||||
private const val SILVER_PLACE_COLOR_LIGHT = 0xFF6680CC
|
||||
private const val BRONZE_PLACE_COLOR_NIGHT = 0xFFFF9976
|
||||
private const val BRONZE_PLACE_COLOR_LIGHT = 0xFFCC7F66
|
||||
|
||||
// region preview
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
|
||||
TangemThemePreviewRedesign {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
var prices by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
100 to PriceChangeType.NEUTRAL,
|
||||
200 to PriceChangeType.NEUTRAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItemV2(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItemV2(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
state1 = state1.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
},
|
||||
) { Text(text = "trend") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
prices = prices.map { (price, _) ->
|
||||
if (Random.nextBoolean()) {
|
||||
price.inc() to PriceChangeType.UP
|
||||
} else {
|
||||
price.dec() to PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text(text = "price") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.common.ui.markets
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
MarketsListItemV2(
|
||||
model = model,
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
MarketsListItemV1(
|
||||
model = model,
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,23 +4,29 @@ import android.content.res.Configuration
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
MarketsListItemPlaceholderV2()
|
||||
} else {
|
||||
MarketsListItemPlaceholderV1()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
private fun MarketsListItemPlaceholderV1() {
|
||||
val density = LocalDensity.current
|
||||
val windowSize = LocalWindowSize.current
|
||||
val sp12 = with(density) { 12.sp.toDp() }
|
||||
|
|
@ -84,11 +90,71 @@ fun MarketsListItemPlaceholder() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun MarketsListItemPlaceholderV2() {
|
||||
val windowSize = LocalWindowSize.current
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 14.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(40.dp))
|
||||
|
||||
SpacerW(4.dp)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(96.dp)
|
||||
.height(20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(46.dp)
|
||||
.height(16.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
SpacerW(10.dp)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(56.dp)
|
||||
.height(20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(46.dp)
|
||||
.height(16.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
private fun PreviewV1() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
repeat(20) {
|
||||
|
|
@ -96,4 +162,20 @@ private fun Preview() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun PreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
CompositionLocalProvider(LocalRedesignEnabled provides true) {
|
||||
Column(Modifier.background(TangemTheme.colors2.surface.level3)) {
|
||||
repeat(20) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,8 +38,8 @@ import com.tangem.utils.StringsSigns.MINUS
|
|||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
fun MarketsListItemV1(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContentV1(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
|
|
@ -50,7 +50,7 @@ fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onC
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
private fun MarketsListItemContentV1(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
|
|
@ -273,11 +273,11 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
|
|||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
MarketsListItemV1(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
MarketsListItemV1(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
|
|
@ -16,7 +16,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
|
|||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
ratingPosition = "1",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
|
|
@ -33,7 +33,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
|
|||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
ratingPosition = "2",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ dependencies {
|
|||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.promo.models)
|
||||
implementation(projects.domain.common)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.promo.models.StoryContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
object SwapStoriesFactory {
|
||||
|
||||
// WARNING! Be careful with indices. Temporary solution.
|
||||
// Use all data from v1/stories api (image url, title, subtitle)
|
||||
@Suppress("MagicNumber")
|
||||
fun createStoriesState(swapStory: StoryContent, onStoriesClose: (Int) -> Unit): SwapStoriesUM {
|
||||
val storyOrderedImageUrls = swapStory.getImageUrls()
|
||||
if (storyOrderedImageUrls.size != 5) return SwapStoriesUM.Empty
|
||||
|
||||
return SwapStoriesUM.Content(
|
||||
stories = persistentListOf(
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[0],
|
||||
title = resourceReference(R.string.swap_story_first_title),
|
||||
subtitle = resourceReference(R.string.swap_story_first_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[1],
|
||||
title = resourceReference(R.string.swap_story_second_title),
|
||||
subtitle = resourceReference(R.string.swap_story_second_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[2],
|
||||
title = resourceReference(R.string.swap_story_third_title),
|
||||
subtitle = resourceReference(R.string.swap_story_third_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[3],
|
||||
title = resourceReference(R.string.swap_story_forth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[4],
|
||||
title = resourceReference(R.string.swap_story_fifth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
|
||||
),
|
||||
),
|
||||
onClose = onStoriesClose,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,12 @@ import androidx.compose.animation.core.tween
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
|
|
@ -18,6 +22,23 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
*/
|
||||
@Composable
|
||||
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TokenPriceTextV2(
|
||||
price = price,
|
||||
modifier = modifier,
|
||||
priceChangeType = priceChangeType,
|
||||
)
|
||||
} else {
|
||||
TokenPriceTextV1(
|
||||
price = price,
|
||||
modifier = modifier,
|
||||
priceChangeType = priceChangeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV1(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
|
@ -52,4 +73,60 @@ fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType
|
|||
style = TangemTheme.typography.body2,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors2.text.status.accent
|
||||
val fallColor = TangemTheme.colors2.text.status.warning
|
||||
val generalColor = TangemTheme.colors2.text.neutral.primary
|
||||
val decimalColor = TangemTheme.colors2.text.neutral.secondary
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
var isAnimationSkipped by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(price) {
|
||||
if (!isAnimationSkipped) {
|
||||
isAnimationSkipped = true
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (priceChangeType != null) {
|
||||
val nextColor = when (priceChangeType) {
|
||||
PriceChangeType.UP -> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
PriceChangeType.NEUTRAL -> return@LaunchedEffect
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
}
|
||||
|
||||
val annotatedText = remember(price) {
|
||||
buildAnnotatedString {
|
||||
val dotIndex = price.indexOf(".")
|
||||
|
||||
if (dotIndex == -1) {
|
||||
append(price)
|
||||
} else {
|
||||
append(price.take(dotIndex))
|
||||
|
||||
withStyle(
|
||||
style = SpanStyle(color = decimalColor),
|
||||
) {
|
||||
append(price.substring(dotIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = annotatedText,
|
||||
color = color.value,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,13 +6,11 @@ import com.tangem.core.abtests.manager.ABTestsManager
|
|||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -24,7 +22,7 @@ internal object ABTestsManagerModule {
|
|||
fun provideABTestsManager(
|
||||
application: Application,
|
||||
environmentConfig: EnvironmentConfig,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): ABTestsManager {
|
||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||
StubABTestsManager()
|
||||
|
|
@ -32,7 +30,7 @@ internal object ABTestsManagerModule {
|
|||
AmplitudeABTestsManager(
|
||||
application = application,
|
||||
apiKey = environmentConfig.amplitudeApiKey,
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import com.amplitude.experiment.ExperimentConfig
|
|||
import com.amplitude.experiment.ExperimentUser
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
val apiKey: String,
|
||||
val scope: CoroutineScope,
|
||||
val scope: AppCoroutineScope,
|
||||
) : ABTestsManager {
|
||||
|
||||
private lateinit var client: ExperimentClient
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ sealed class MainScreenAnalyticsEvent(
|
|||
event = "Notice - Limits Info",
|
||||
)
|
||||
|
||||
class ButtonQrScan : MainScreenAnalyticsEvent(
|
||||
event = "Button - QR Scan",
|
||||
)
|
||||
|
||||
class ButtonExplore : MainScreenAnalyticsEvent(
|
||||
event = "Button - Explore",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import org.json.JSONArray
|
||||
import com.tangem.plugin.configuration.configurations.TogglesGenerator
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -11,10 +9,31 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.squareup:kotlinpoet:1.15.0")
|
||||
classpath("org.json:json:20231013")
|
||||
abstract class GenerateTogglesTask : DefaultTask() {
|
||||
|
||||
@get:InputFiles
|
||||
abstract val configFiles: ConfigurableFileCollection
|
||||
|
||||
@get:Input
|
||||
abstract val fileNames: ListProperty<String>
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val files = configFiles.files.toList()
|
||||
val names = fileNames.get()
|
||||
|
||||
require(files.size == names.size) {
|
||||
"configFiles (${files.size}) and fileNames (${names.size}) must have the same size"
|
||||
}
|
||||
|
||||
files.zip(names).forEach { (inputFile, fileName) ->
|
||||
require(inputFile.exists()) { "Config file not found: ${inputFile.absolutePath}" }
|
||||
logger.lifecycle("Generating toggles from ${inputFile.name}")
|
||||
TogglesGenerator.generate(inputFile, outputDir.get().asFile, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,8 +42,20 @@ android {
|
|||
sourceSets["main"].java.srcDir("build/generated/source/toggles")
|
||||
}
|
||||
|
||||
/** Config file to generated enum class name mapping */
|
||||
val toggles = mapOf(
|
||||
file("src/main/assets/configs/feature_toggles_config.json") to "FeatureToggles",
|
||||
file("src/main/assets/configs/excluded_blockchains_config.json") to "ExcludedBlockchainToggles",
|
||||
)
|
||||
|
||||
val generateToggles = tasks.register<GenerateTogglesTask>("generateToggles") {
|
||||
configFiles.from(toggles.keys)
|
||||
fileNames.set(toggles.values.toList())
|
||||
outputDir.set(layout.buildDirectory.dir("generated/source/toggles"))
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
|
||||
dependsOn(generateToggles)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
|
|
@ -51,77 +82,4 @@ dependencies {
|
|||
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/feature_toggles_config.json",
|
||||
generatedFileName = "FeatureToggles",
|
||||
)
|
||||
}
|
||||
|
||||
val generateExcludedBlockchainToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json",
|
||||
generatedFileName = "ExcludedBlockchainToggles",
|
||||
)
|
||||
}
|
||||
|
||||
fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
||||
val inputFile = file(inputFilePath)
|
||||
val outputDir = file("build/generated/source/toggles")
|
||||
|
||||
inputs.file(inputFile)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val jsonText = inputFile.readText()
|
||||
val jsonArray = JSONArray(jsonText)
|
||||
|
||||
val entries = (0 until jsonArray.length()).map { i ->
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val name = obj.getString("name")
|
||||
val version = obj.getString("version")
|
||||
CodeBlock.of("%S to %S", name, version)
|
||||
}
|
||||
|
||||
val mapInitializer = CodeBlock.builder()
|
||||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEach { entry ->
|
||||
add(entry)
|
||||
add(",\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
.add(")")
|
||||
.build()
|
||||
|
||||
val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName)
|
||||
.addKdoc("Generated from $inputFilePath")
|
||||
.addProperty(
|
||||
PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING))
|
||||
.initializer(mapInitializer)
|
||||
.build()
|
||||
)
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName)
|
||||
.addType(objectBuilder.build())
|
||||
.build()
|
||||
|
||||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
|
||||
// Remove redundant public visibility modifiers
|
||||
val generatedFile = File(outputPackageDir, "com/tangem/core/configtoggle/$generatedFileName.kt")
|
||||
if (generatedFile.exists()) {
|
||||
val content = generatedFile.readText()
|
||||
val fixedContent = content
|
||||
.replace("public object ", "object ")
|
||||
.replace("public val ", "val ")
|
||||
generatedFile.writeText(fixedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ </ID>
|
||||
<ID>Indentation:FeatureToggles.kt$FeatureToggles$ </ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -59,5 +59,21 @@
|
|||
{
|
||||
"name": "CUSTOMER_IO_ENABLED",
|
||||
"version": "5.35"
|
||||
},
|
||||
{
|
||||
"name": "MAIN_SCREEN_QR_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_PROMO_BANNERS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_SYNC_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
|
|
@ -12,11 +12,13 @@ import kotlin.properties.Delegates
|
|||
/**
|
||||
* [MutableExcludedBlockchainsManager] implementation in dev or mocked build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property localTogglesStorage local storage for blockchain toggles
|
||||
* @property versionProvider application version provider
|
||||
* @property excludedBlockchainTogglesProvider provider for excluded blockchain toggle entries
|
||||
* @property localTogglesStorage local storage for blockchain toggles
|
||||
*/
|
||||
internal class DevExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val excludedBlockchainTogglesProvider: ExcludedBlockchainTogglesProvider,
|
||||
private val localTogglesStorage: LocalTogglesStorage,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
|
|
@ -63,6 +65,7 @@ internal class DevExcludedBlockchainsManager(
|
|||
private fun getFileBlockchainToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
return excludedBlockchainTogglesProvider.getToggles()
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* [ExcludedBlockchainsManager] implementation in PROD build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property versionProvider application version provider
|
||||
* @property excludedBlockchainTogglesProvider provider for excluded blockchain toggle entries
|
||||
*/
|
||||
internal class ProdExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val excludedBlockchainTogglesProvider: ExcludedBlockchainTogglesProvider,
|
||||
) : ExcludedBlockchainsManager {
|
||||
|
||||
override val excludedBlockchainsIds: Set<String> = getBlockchainToggles()
|
||||
|
|
@ -19,7 +21,7 @@ internal class ProdExcludedBlockchainsManager(
|
|||
private fun getBlockchainToggles(): Set<String> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values
|
||||
return excludedBlockchainTogglesProvider.getToggles()
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
.filterValues { !it }
|
||||
.keys
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.configtoggle.blockchain.provider
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [ExcludedBlockchainTogglesProvider] that reads from the generated
|
||||
* [ExcludedBlockchainToggles] enum.
|
||||
*/
|
||||
internal class DefaultExcludedBlockchainTogglesProvider @Inject constructor() : ExcludedBlockchainTogglesProvider {
|
||||
|
||||
override fun getToggles(): Map<String, String> {
|
||||
return ExcludedBlockchainToggles.entries.associate { it.rawName to it.version }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.core.configtoggle.blockchain.provider
|
||||
|
||||
/**
|
||||
* Provider for excluded blockchain toggle entries.
|
||||
* This interface allows for easier testing by enabling mock implementations.
|
||||
*/
|
||||
internal interface ExcludedBlockchainTogglesProvider {
|
||||
|
||||
/**
|
||||
* Returns a map of blockchain IDs to their version strings.
|
||||
* The version string is "undefined" for disabled toggles, or a version number like "5.21.0" for enabled ones.
|
||||
*/
|
||||
fun getToggles(): Map<String, String>
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig
|
|||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.provider.DefaultExcludedBlockchainTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -26,17 +27,22 @@ internal object ExcludedBlockchainsManagerModule {
|
|||
appPreferencesStore: AppPreferencesStore,
|
||||
): ExcludedBlockchainsManager {
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
val excludedBlockchainTogglesProvider = DefaultExcludedBlockchainTogglesProvider()
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevExcludedBlockchainsManager(
|
||||
versionProvider = versionProvider,
|
||||
excludedBlockchainTogglesProvider = excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdExcludedBlockchainsManager(versionProvider = versionProvider)
|
||||
ProdExcludedBlockchainsManager(
|
||||
versionProvider = versionProvider,
|
||||
excludedBlockchainTogglesProvider = excludedBlockchainTogglesProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig
|
|||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.provider.DefaultFeatureTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -26,17 +27,22 @@ internal object FeatureTogglesManagerModule {
|
|||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
val featureTogglesProvider = DefaultFeatureTogglesProvider()
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
versionProvider = versionProvider,
|
||||
featureTogglesProvider = featureTogglesProvider,
|
||||
featureTogglesLocalStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdFeatureTogglesManager(versionProvider = versionProvider)
|
||||
ProdFeatureTogglesManager(
|
||||
versionProvider = versionProvider,
|
||||
featureTogglesProvider = featureTogglesProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.core.configtoggle.feature
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
|
||||
/**
|
||||
* Component for getting information about the availability of feature toggles
|
||||
*
|
||||
|
|
@ -7,6 +9,6 @@ package com.tangem.core.configtoggle.feature
|
|||
*/
|
||||
interface FeatureTogglesManager {
|
||||
|
||||
/** Check feature toggle availability by name [name] */
|
||||
fun isFeatureEnabled(name: String): Boolean
|
||||
/** Check feature [toggle] availability */
|
||||
fun isFeatureEnabled(toggle: FeatureToggles): Boolean
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.configtoggle.feature.impl
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
|
|
@ -14,10 +15,12 @@ import kotlin.properties.Delegates
|
|||
* Feature toggles manager implementation in dev or mocked build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property featureTogglesProvider provider for feature toggle entries
|
||||
* @property featureTogglesLocalStorage local storage for feature toggles
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val featureTogglesProvider: FeatureTogglesProvider,
|
||||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
|
|
@ -36,7 +39,10 @@ internal class DevFeatureTogglesManager(
|
|||
.toMutableMap()
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] == true
|
||||
override fun isFeatureEnabled(toggle: FeatureToggles): Boolean = featureTogglesMap[toggle.rawName] == true
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun isFeatureEnabledByName(name: String): Boolean = featureTogglesMap[name] == true
|
||||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
|
|
@ -60,7 +66,8 @@ internal class DevFeatureTogglesManager(
|
|||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
return featureTogglesProvider.getToggles()
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
|
|
|
|||
|
|
@ -3,26 +3,33 @@ package com.tangem.core.configtoggle.feature.impl
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in PROD build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property versionProvider application version provider
|
||||
* @property featureTogglesProvider provider for feature toggle entries
|
||||
*/
|
||||
internal class ProdFeatureTogglesManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val featureTogglesProvider: FeatureTogglesProvider,
|
||||
) : FeatureTogglesManager {
|
||||
|
||||
private val featureToggles: Map<String, Boolean> = getFileFeatureToggles()
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true
|
||||
override fun isFeatureEnabled(toggle: FeatureToggles): Boolean = featureToggles[toggle.rawName] == true
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun isFeatureEnabledByName(name: String): Boolean = featureToggles[name] == true
|
||||
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
return featureTogglesProvider.getToggles()
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.core.configtoggle.feature.provider
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [FeatureTogglesProvider] that reads from the generated [FeatureToggles] enum.
|
||||
*/
|
||||
internal class DefaultFeatureTogglesProvider @Inject constructor() : FeatureTogglesProvider {
|
||||
|
||||
override fun getToggles(): Map<String, String> {
|
||||
return FeatureToggles.entries.associate { it.rawName to it.version }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.core.configtoggle.feature.provider
|
||||
|
||||
/**
|
||||
* Provider for feature toggle entries.
|
||||
* This interface allows for easier testing by enabling mock implementations.
|
||||
*/
|
||||
internal interface FeatureTogglesProvider {
|
||||
|
||||
/**
|
||||
* Returns a map of toggle names to their version strings.
|
||||
* The version string is "undefined" for disabled toggles, or a version number like "1.0.0" for enabled ones.
|
||||
*/
|
||||
fun getToggles(): Map<String, String>
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -12,42 +12,74 @@ import org.junit.jupiter.api.*
|
|||
internal class DevExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val excludedBlockchainTogglesProvider = mockk<ExcludedBlockchainTogglesProvider>()
|
||||
private val localTogglesStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
private val testVersion = "5.30"
|
||||
|
||||
private val testToggles = mapOf(
|
||||
"ENABLED_BLOCKCHAIN" to "5.21.0",
|
||||
"DISABLED_BLOCKCHAIN" to "undefined",
|
||||
"FUTURE_BLOCKCHAIN" to "6.0.0",
|
||||
)
|
||||
|
||||
private fun getExpectedFileToggles(appVersion: String?): Map<String, Boolean> {
|
||||
if (appVersion.isNullOrEmpty()) {
|
||||
return testToggles.mapValues { false }
|
||||
}
|
||||
return testToggles.mapValues { (_, version) ->
|
||||
version != "undefined" && isVersionSufficient(appVersion, version)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExcludedBlockchains(appVersion: String?): Set<String> {
|
||||
return getExpectedFileToggles(appVersion).filterValues { !it }.keys
|
||||
}
|
||||
|
||||
private fun isVersionSufficient(appVersion: String, requiredVersion: String): Boolean {
|
||||
val appParts = appVersion.split(".").mapNotNull { it.toIntOrNull() }
|
||||
val reqParts = requiredVersion.split(".").mapNotNull { it.toIntOrNull() }
|
||||
for (i in 0 until maxOf(appParts.size, reqParts.size)) {
|
||||
val app = appParts.getOrElse(i) { 0 }
|
||||
val req = reqParts.getOrElse(i) { 0 }
|
||||
if (app > req) return true
|
||||
if (app < req) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true)
|
||||
val appVersion = testVersion
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val savedToggles = fileToggles.mapValues { !it.value }
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
val actual = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1")
|
||||
val expected = savedToggles.filterValues { !it }.keys
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -59,14 +91,20 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
val appVersion: String? = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
val actual = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
|
|
@ -77,14 +115,20 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
val appVersion = testVersion
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
val actual = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_2")
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
|
|
@ -95,13 +139,19 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
val appVersion = testVersion
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) }
|
||||
.exceptionOrNull()!!
|
||||
val actual = runCatching {
|
||||
DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
)
|
||||
}.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
|
|
@ -118,21 +168,34 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ExcludeBlockchain {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun excludeBlockchain_changesStatusAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
every { versionProvider.get() } returns testVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
val manager = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
)
|
||||
|
||||
// Act
|
||||
manager.excludeBlockchain("ENABLED_BLOCKCHAIN", false)
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).contains("CHAIN_1")
|
||||
coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) }
|
||||
Truth.assertThat(actual).contains("ENABLED_BLOCKCHAIN")
|
||||
coVerify { localTogglesStorage.store(match { it["ENABLED_BLOCKCHAIN"] == false }) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,13 +203,26 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
every { versionProvider.get() } returns testVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
val manager = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
|
@ -158,13 +234,17 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
every { versionProvider.get() } returns testVersion
|
||||
val fileToggles = getExpectedFileToggles(testVersion)
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
val manager = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
)
|
||||
|
||||
// Act
|
||||
manager.excludeBlockchain("ENABLED_BLOCKCHAIN", !fileToggles.getValue("ENABLED_BLOCKCHAIN"))
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
|
|
@ -176,28 +256,36 @@ internal class DevExcludedBlockchainsManagerTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig_resetsToFileAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true)
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.recoverLocalConfig()
|
||||
val appVersion = testVersion
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
val manager = DevExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
localTogglesStorage,
|
||||
)
|
||||
manager.excludeBlockchain("ENABLED_BLOCKCHAIN", false)
|
||||
|
||||
// Act
|
||||
manager.recoverLocalConfig()
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
|
|
@ -11,39 +14,65 @@ import org.junit.jupiter.api.*
|
|||
internal class ProdExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val excludedBlockchainTogglesProvider = mockk<ExcludedBlockchainTogglesProvider>()
|
||||
|
||||
private val testVersion = "5.30"
|
||||
|
||||
private val testToggles = mapOf(
|
||||
"ENABLED_BLOCKCHAIN" to "5.21.0",
|
||||
"DISABLED_BLOCKCHAIN" to "undefined",
|
||||
"FUTURE_BLOCKCHAIN" to "6.0.0",
|
||||
)
|
||||
|
||||
private fun getExcludedBlockchains(appVersion: String?): Set<String> {
|
||||
if (appVersion.isNullOrEmpty()) {
|
||||
return testToggles.keys
|
||||
}
|
||||
return testToggles.filterValues { version ->
|
||||
version == "undefined" || !isVersionSufficient(appVersion, version)
|
||||
}.keys
|
||||
}
|
||||
|
||||
private fun isVersionSufficient(appVersion: String, requiredVersion: String): Boolean {
|
||||
val appParts = appVersion.split(".").mapNotNull { it.toIntOrNull() }
|
||||
val reqParts = requiredVersion.split(".").mapNotNull { it.toIntOrNull() }
|
||||
for (i in 0 until maxOf(appParts.size, reqParts.size)) {
|
||||
val app = appParts.getOrElse(i) { 0 }
|
||||
val req = reqParts.getOrElse(i) { 0 }
|
||||
if (app > req) return true
|
||||
if (app < req) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
clearMocks(versionProvider, excludedBlockchainTogglesProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize excluded blockchains`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val appVersion = testVersion
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
val actual = ProdExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_2")
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -52,13 +81,17 @@ internal class ProdExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
val appVersion: String? = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
val actual = ProdExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -67,13 +100,17 @@ internal class ProdExcludedBlockchainsManagerTest {
|
|||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns ""
|
||||
val appVersion = ""
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
val actual = ProdExcludedBlockchainsManager(
|
||||
versionProvider,
|
||||
excludedBlockchainTogglesProvider,
|
||||
).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
val expected = getExcludedBlockchains(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -86,7 +123,9 @@ internal class ProdExcludedBlockchainsManagerTest {
|
|||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!!
|
||||
val actual = runCatching {
|
||||
ProdExcludedBlockchainsManager(versionProvider, excludedBlockchainTogglesProvider)
|
||||
}.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel
|
||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
|
|
@ -12,51 +11,55 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val featureTogglesProvider = mockk<FeatureTogglesProvider>()
|
||||
private val featureTogglesLocalStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
private val testToggles = mapOf(
|
||||
"ENABLED_TOGGLE" to "1.0.0",
|
||||
"DISABLED_TOGGLE" to "undefined",
|
||||
)
|
||||
|
||||
private fun getExpectedFileToggles(appVersion: String?): Map<String, Boolean> =
|
||||
testToggles.mapValues { (_, version) ->
|
||||
version != "undefined" && !appVersion.isNullOrEmpty()
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val savedToggles = fileToggles.mapValues { !it.value }
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
val actual = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
val expected = savedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -68,17 +71,21 @@ internal class DevFeatureTogglesManagerTest {
|
|||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
val appVersion: String? = null
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val savedToggles = fileToggles.mapValues { !it.value }
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
val actual = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
val expected = savedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -91,16 +98,20 @@ internal class DevFeatureTogglesManagerTest {
|
|||
fun `successfully initialize manager if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val savedToggles = fileToggles.mapValues { !it.value }
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
val actual = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
val expected = savedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -116,8 +127,13 @@ internal class DevFeatureTogglesManagerTest {
|
|||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
val actual = runCatching {
|
||||
DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
)
|
||||
}.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
|
|
@ -131,16 +147,18 @@ internal class DevFeatureTogglesManagerTest {
|
|||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = emptyMap<String, Boolean>()
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
val actual = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
val expected = getExpectedFileToggles(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -153,16 +171,20 @@ internal class DevFeatureTogglesManagerTest {
|
|||
fun `successfully initialize manager if storage returns unknown toggles`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_3" to true)
|
||||
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val unknownSavedToggles = fileToggles + ("UNKNOWN_TOGGLE" to true)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns unknownSavedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
val actual = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
val expected = fileToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -176,13 +198,17 @@ internal class DevFeatureTogglesManagerTest {
|
|||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
val actual = runCatching {
|
||||
DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
)
|
||||
}.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
|
|
@ -203,65 +229,64 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
val savedToggles = mapOf(
|
||||
"DISABLED_TOGGLE" to true,
|
||||
"ENABLED_TOGGLE" to false,
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
val actual = manager.isFeatureEnabledByName(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
IsFeatureEnabledModel(name = "ENABLED_TOGGLE", expected = false),
|
||||
IsFeatureEnabledModel(name = "DISABLED_TOGGLE", expected = true),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsFeatureEnabledModel(val name: String, val expected: Boolean)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.fileFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply {
|
||||
val manager = DevFeatureTogglesManager(
|
||||
versionProvider,
|
||||
featureTogglesProvider,
|
||||
featureTogglesLocalStorage,
|
||||
).apply {
|
||||
setFeatureToggles(model.storedFeatureToggles.toMutableMap())
|
||||
}
|
||||
|
||||
|
|
@ -269,54 +294,45 @@ internal class DevFeatureTogglesManagerTest {
|
|||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = emptyMap(),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
private fun provideTestModels(): List<IsMatchLocalConfigModel> {
|
||||
val appVersion = "1.0.0"
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val firstToggleName = "ENABLED_TOGGLE"
|
||||
|
||||
return listOf(
|
||||
IsMatchLocalConfigModel(
|
||||
storedFeatureToggles = fileToggles,
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
storedFeatureToggles = fileToggles.mapValues { !it.value },
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
storedFeatureToggles = mapOf(firstToggleName to fileToggles.getValue(firstToggleName)),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
storedFeatureToggles = mapOf("UNKNOWN_TOGGLE" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
storedFeatureToggles = emptyMap(),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class IsMatchLocalConfigModel(
|
||||
val fileFeatureToggles: Map<String, String>,
|
||||
val storedFeatureToggles: Map<String, Boolean>,
|
||||
val expected: Boolean,
|
||||
)
|
||||
|
|
@ -325,41 +341,40 @@ internal class DevFeatureTogglesManagerTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetFeatureToggles {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
val appVersion = "1.0.0"
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val savedToggles = mapOf(
|
||||
"DISABLED_TOGGLE" to true,
|
||||
"ENABLED_TOGGLE" to false,
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
val expected = fileToggles + savedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,25 +382,30 @@ internal class DevFeatureTogglesManagerTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ChangeToggle {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun changeToggle(model: ChangeToggleModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.initialToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.changeToggle(name = model.name, isEnabled = model.isEnabled)
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = model.expectedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(model.expectedToggles)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
|
|
@ -395,77 +415,69 @@ internal class DevFeatureTogglesManagerTest {
|
|||
featureTogglesLocalStorage.store(model.expectedToggles)
|
||||
}
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = false,
|
||||
expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_2",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_3",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = emptyMap(),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = true,
|
||||
expectedToggles = emptyMap(),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
)
|
||||
private fun provideTestModels(): List<ChangeToggleModel> {
|
||||
val appVersion = "1.0.0"
|
||||
val fileToggles = getExpectedFileToggles(appVersion)
|
||||
val enabledName = "ENABLED_TOGGLE"
|
||||
val disabledName = "DISABLED_TOGGLE"
|
||||
|
||||
return listOf(
|
||||
ChangeToggleModel(
|
||||
name = enabledName,
|
||||
isEnabled = false,
|
||||
expectedToggles = fileToggles + (enabledName to false),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
name = disabledName,
|
||||
isEnabled = true,
|
||||
expectedToggles = fileToggles + (disabledName to true),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
name = "UNKNOWN_TOGGLE",
|
||||
isEnabled = true,
|
||||
expectedToggles = fileToggles,
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
val appVersion = "1.0.0"
|
||||
val savedToggles = mapOf(
|
||||
"DISABLED_TOGGLE" to true,
|
||||
"ENABLED_TOGGLE" to false,
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.recoverLocalConfig()
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
val expected = getExpectedFileToggles(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -473,14 +485,10 @@ internal class DevFeatureTogglesManagerTest {
|
|||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
featureTogglesLocalStorage.store(expected)
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeToggleModel(
|
||||
val initialToggles: Map<String, String>,
|
||||
val name: String,
|
||||
val isEnabled: Boolean,
|
||||
val expectedToggles: Map<String, Boolean>,
|
||||
|
|
|
|||
|
|
@ -1,43 +1,47 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val featureTogglesProvider = mockk<FeatureTogglesProvider>()
|
||||
|
||||
private val testToggles = mapOf(
|
||||
"ENABLED_TOGGLE" to "1.0.0",
|
||||
"DISABLED_TOGGLE" to "undefined",
|
||||
)
|
||||
|
||||
private fun getExpectedToggles(appVersion: String?): Map<String, Boolean> {
|
||||
return testToggles.mapValues { (_, version) ->
|
||||
version != "undefined" && !appVersion.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
clearMocks(versionProvider, featureTogglesProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -47,10 +51,10 @@ internal class ProdFeatureTogglesManagerTest {
|
|||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
val expected = getExpectedToggles(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -59,14 +63,14 @@ internal class ProdFeatureTogglesManagerTest {
|
|||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
val appVersion: String? = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
val expected = getExpectedToggles(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -79,10 +83,10 @@ internal class ProdFeatureTogglesManagerTest {
|
|||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
val expected = getExpectedToggles(appVersion)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
|
|
@ -95,7 +99,8 @@ internal class ProdFeatureTogglesManagerTest {
|
|||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!!
|
||||
val actual = runCatching { ProdFeatureTogglesManager(versionProvider, featureTogglesProvider) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
|
|
@ -113,41 +118,29 @@ internal class ProdFeatureTogglesManagerTest {
|
|||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { featureTogglesProvider.getToggles() } returns testToggles
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
manager = ProdFeatureTogglesManager(versionProvider)
|
||||
manager = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider)
|
||||
unmockkObject(FeatureToggles)
|
||||
clearMocks(versionProvider, featureTogglesProvider)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
val actual = manager.isFeatureEnabledByName(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
IsFeatureEnabledModel(name = "ENABLED_TOGGLE", expected = true),
|
||||
IsFeatureEnabledModel(name = "DISABLED_TOGGLE", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
|
|
@ -22,7 +20,7 @@ import kotlinx.coroutines.flow.*
|
|||
internal class DevApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appScope: AppCoroutineScope,
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
|
@ -51,7 +49,7 @@ internal class DevApiConfigsManager(
|
|||
|
||||
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
|
||||
}
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,22 @@ import com.tangem.datasource.api.common.config.ApiConfig
|
|||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.Boolean
|
||||
import kotlin.String
|
||||
import kotlin.Unit
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.any
|
||||
import kotlin.collections.associateWith
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.first
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.mapValues
|
||||
import kotlin.collections.plus
|
||||
import kotlin.error
|
||||
import kotlin.to
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in MOCK environment
|
||||
|
|
@ -18,7 +30,7 @@ import kotlinx.coroutines.flow.*
|
|||
*/
|
||||
internal class MockApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
|
@ -26,8 +38,6 @@ internal class MockApiConfigsManager(
|
|||
|
||||
override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ interface TangemExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromAmount") fromAmount: String?,
|
||||
@Query("toAmount") toAmount: String? = null,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
|
|
@ -57,7 +58,8 @@ interface TangemExpressApi {
|
|||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("fromAddress") fromAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromAmount") fromAmount: String?,
|
||||
@Query("toAmount") toAmount: String? = null,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ interface TangemPayApi {
|
|||
@POST("v1/deeplink/validate")
|
||||
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
|
||||
|
||||
@GET("v1/customer/eligibility")
|
||||
suspend fun checkCustomerEligibility(): ApiResponse<CustomerEligibilityResponse>
|
||||
@GET("v1/eligibility/channels")
|
||||
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
@GET("v1/order/{order_id}")
|
||||
suspend fun getOrder(
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerEligibilityResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
data class TangemPayEligibilityChannels(
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean,
|
||||
@Json(name = "channels") val channels: List<String>,
|
||||
)
|
||||
}
|
||||
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