Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-18 14:12:15 +03:00
commit 20279dcc7a
602 changed files with 18887 additions and 6569 deletions

View file

@ -366,6 +366,7 @@ dependencies {
implementation(deps.googlePlay.advertising) implementation(deps.googlePlay.advertising)
coreLibraryDesugaring(deps.desugar) coreLibraryDesugaring(deps.desugar)
implementation(deps.timber) implementation(deps.timber)
implementation(deps.kermit)
implementation(deps.reKotlin) implementation(deps.reKotlin)
implementation(deps.zxing.qrCore) implementation(deps.zxing.qrCore)
implementation(deps.coil) implementation(deps.coil)

View file

@ -1,6 +1,7 @@
package com.tangem.common package com.tangem.common
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.tap.ApplicationEntryPoint import com.tangem.tap.ApplicationEntryPoint
import com.tangem.tap.TangemApplication import com.tangem.tap.TangemApplication
import dagger.hilt.android.testing.OnComponentReadyRunner import dagger.hilt.android.testing.OnComponentReadyRunner
@ -10,7 +11,7 @@ import org.junit.runners.model.Statement
import timber.log.Timber import timber.log.Timber
class ApplicationInjectionExecutionRule( class ApplicationInjectionExecutionRule(
private val toggleStates: Map<String, Boolean> private val toggleStates: Map<String, Boolean>,
) : TestRule { ) : TestRule {
private val tangemApplication: TangemApplication private val tangemApplication: TangemApplication
@ -25,7 +26,7 @@ class ApplicationInjectionExecutionRule(
overrideFeatureToggles() overrideFeatureToggles()
OnComponentReadyRunner.addListener( OnComponentReadyRunner.addListener(
tangemApplication, ApplicationEntryPoint::class.java tangemApplication, ApplicationEntryPoint::class.java,
) { _: ApplicationEntryPoint -> ) { _: ApplicationEntryPoint ->
tangemApplication.preInit() tangemApplication.preInit()
tangemApplication.init() tangemApplication.init()
@ -43,25 +44,15 @@ class ApplicationInjectionExecutionRule(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun saveOriginalFeatureToggles() { private fun saveOriginalFeatureToggles() {
try { try {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") originalFeatureTogglesValues = FeatureToggles.values as Map<String, String>
val valuesField = featureTogglesClass.getDeclaredField("values")
valuesField.isAccessible = true
originalFeatureTogglesValues = valuesField.get(null) as Map<String, String>
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Failed to save original toggles values: ${e.message}") Timber.e("Failed to save original toggles values: ${e.message}")
} }
} }
@Suppress("UNCHECKED_CAST")
private fun overrideFeatureToggles() { private fun overrideFeatureToggles() {
try { try {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") val originalValues = originalFeatureTogglesValues ?: FeatureToggles.values
val valuesField = featureTogglesClass.getDeclaredField("values")
valuesField.isAccessible = true
val originalValues = originalFeatureTogglesValues ?:
(valuesField.get(null) as Map<String, String>)
val newValues = originalValues.toMutableMap() val newValues = originalValues.toMutableMap()
toggleStates.forEach { (toggle, enabled) -> toggleStates.forEach { (toggle, enabled) ->
@ -72,10 +63,12 @@ class ApplicationInjectionExecutionRule(
} }
} }
valuesField.set(null, newValues) val companionClass = FeatureToggles.Companion::class.java
val valuesField = companionClass.getDeclaredField("values")
valuesField.isAccessible = true
valuesField.set(FeatureToggles.Companion, newValues)
Timber.i("FeatureToggles.values updated: $toggleStates") Timber.i("FeatureToggles.values updated: $toggleStates")
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("FeatureToggles.values didn't change with error: ${e.message}") Timber.e("FeatureToggles.values didn't change with error: ${e.message}")
} }
@ -84,10 +77,10 @@ class ApplicationInjectionExecutionRule(
private fun restoreOriginalFeatureToggles() { private fun restoreOriginalFeatureToggles() {
try { try {
if (originalFeatureTogglesValues != null) { if (originalFeatureTogglesValues != null) {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") val companionClass = FeatureToggles.Companion::class.java
val valuesField = featureTogglesClass.getDeclaredField("values") val valuesField = companionClass.getDeclaredField("values")
valuesField.isAccessible = true valuesField.isAccessible = true
valuesField.set(null, originalFeatureTogglesValues) valuesField.set(FeatureToggles.Companion, originalFeatureTogglesValues)
Timber.i("FeatureToggles.values restored") Timber.i("FeatureToggles.values restored")
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.runBlocking
import org.junit.Rule import org.junit.Rule
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import javax.inject.Inject import javax.inject.Inject
abstract class BaseTestCase : TestCase( abstract class BaseTestCase : TestCase(
@ -68,6 +70,12 @@ abstract class BaseTestCase : TestCase(
*/ */
val composeTestRule = createEmptyComposeRule() val composeTestRule = createEmptyComposeRule()
private val semanticTreePrinterRule = object : TestWatcher() {
override fun failed(e: Throwable?, description: Description?) {
runCatching { printAllRoots() }
}
}
@Rule @Rule
@JvmField @JvmField
val ruleChain: TestRule = RuleChain val ruleChain: TestRule = RuleChain
@ -76,6 +84,7 @@ abstract class BaseTestCase : TestCase(
.around(permissionRule) .around(permissionRule)
.around(apiEnvironmentRule) .around(apiEnvironmentRule)
.around(composeTestRule) .around(composeTestRule)
.around(semanticTreePrinterRule)
/** /**
* Initialization order is important: * Initialization order is important:
@ -140,6 +149,16 @@ abstract class BaseTestCase : TestCase(
.printToLog(tag, maxDepth) .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() fun waitForIdle() = composeTestRule.waitForIdle()
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule { private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {

View file

@ -1,10 +1,14 @@
package com.tangem.common.extensions package com.tangem.common.extensions
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsMatcher
import com.tangem.common.utils.LazyListItemNode import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.components.buttons.actions.HasBadgeKey import com.tangem.core.ui.components.buttons.actions.HasBadgeKey
import com.tangem.core.ui.components.buttons.actions.IsDimmedKey import com.tangem.core.ui.components.buttons.actions.IsDimmedKey
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
fun assertElementDoesNotExist( fun assertElementDoesNotExist(
elementProvider: () -> KNode, elementProvider: () -> KNode,
@ -42,4 +46,51 @@ fun Any.assertHasBadge(expectedValue: Boolean = true) {
is KNode, is LazyListItemNode -> this.assert(matcher) is KNode, is LazyListItemNode -> this.assert(matcher)
else -> throw IllegalArgumentException("Unsupported type: ${this::class}") 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()
} }

View file

@ -1,6 +1,8 @@
package com.tangem.common.extensions package com.tangem.common.extensions
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
fun KNode.clickWithAssertion() { fun KNode.clickWithAssertion() {
@ -34,3 +36,34 @@ fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
assertIsNotDisplayed() 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)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.common.extensions package com.tangem.common.extensions
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.wallet.R import com.tangem.wallet.R
@ -46,7 +47,23 @@ fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
} }
fun BaseTestCase.openTheAppFromRecents() { fun BaseTestCase.openTheAppFromRecents() {
device.uiDevice.waitForIdle()
device.uiDevice.pressRecentApps() 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 centerX = device.uiDevice.displayWidth / 2
val centerY = device.uiDevice.displayHeight / 3 val centerY = device.uiDevice.displayHeight / 3
@ -54,6 +71,14 @@ fun BaseTestCase.openTheAppFromRecents() {
device.uiDevice.click(centerX, centerY) 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() { fun BaseTestCase.disableWiFi() {
device.uiDevice.executeShellCommand("svc wifi disable") device.uiDevice.executeShellCommand("svc wifi disable")
} }

View file

@ -1,8 +1,12 @@
package com.tangem.scenarios package com.tangem.scenarios
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeMarketsBlock
import com.tangem.common.extensions.swipeVertical
import com.tangem.screens.onMainScreen import com.tangem.screens.onMainScreen
import com.tangem.screens.onMarketsExchangesScreen
import com.tangem.screens.onMarketsScreen import com.tangem.screens.onMarketsScreen
import com.tangem.screens.onMarketsTokenDetailsScreen import com.tangem.screens.onMarketsTokenDetailsScreen
import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.Allure.step
@ -23,4 +27,55 @@ fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName:
waitForIdle() waitForIdle()
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() } 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()
}
} }

View file

@ -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)

View file

@ -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)

View file

@ -35,6 +35,25 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true 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 { fun tokenWithTitle(title: String): KNode {
return child { return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM) hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)

View file

@ -23,6 +23,11 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true useUnmergedTree = true
} }
val addressesShimmer: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESSES_SHIMMER)
useUnmergedTree = true
}
val topAppBarTitle: KNode = child { val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE) hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(CoreUiR.string.common_address)) hasText(getResourceString(CoreUiR.string.common_address))
@ -62,7 +67,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
} }
val clearTextFieldButton: KNode = child { val clearTextFieldButton: KNode = child {
hasContentDescription(getResourceString(CoreUiR.string.common_close)) hasTestTag(SendAddressScreenTestTags.CROSS_ICON)
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.test.AppBarWithSearchTestTags import com.tangem.core.ui.test.AppBarWithSearchTestTags
import com.tangem.core.ui.test.BuyTokenScreenTestTags 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
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
@ -46,6 +47,13 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
} }
useUnmergedTree = true useUnmergedTree = true
} }
fun marketsTokenWithTitle(title: String): KNode {
return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
hasText(title)
}
}
} }
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =

View file

@ -3,10 +3,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.test.SearchBarTestTags import com.tangem.core.ui.test.*
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
@ -48,6 +45,11 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
useUnmergedTree = true useUnmergedTree = true
} }
val searchBarBlock: KNode = child {
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
useUnmergedTree = true
}
val searchBarIcon: KNode = child { val searchBarIcon: KNode = child {
hasTestTag(SearchBarTestTags.ICON) hasTestTag(SearchBarTestTags.ICON)
useUnmergedTree = true useUnmergedTree = true
@ -58,12 +60,28 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
useUnmergedTree = true 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 { fun tokenWithName(tokenName: String): KNode = child {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE) hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
hasAnyChild(withText(tokenName)) hasAnyChild(withText(tokenName))
useUnmergedTree = true useUnmergedTree = true
} }
fun marketsTokenWithName(title: String): KNode {
return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
hasText(title)
}
}
} }
internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) = internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) =

View file

@ -52,8 +52,8 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
} }
val swapTokensOnscreenButton: KNode = child { val replaceTokensButton: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON) hasTestTag(SwapTokenScreenTestTags.REPLACE_TOKENS_BUTTON)
} }
val receiveAmount: KNode = child { val receiveAmount: KNode = child {

View file

@ -14,6 +14,7 @@ class TopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
) { ) {
val moreButton: KNode = child { val moreButton: KNode = child {
hasTestTag(MainScreenTestTags.MORE_BUTTON) hasTestTag(MainScreenTestTags.MORE_BUTTON)
useUnmergedTree = true
} }
} }

View file

@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickAndWaitFor
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.scenarios.checkFailedTransactionDialog import com.tangem.scenarios.checkFailedTransactionDialog
@ -18,6 +19,7 @@ import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -45,6 +47,7 @@ class FeedbackTest : BaseTestCase() {
onTopBar { moreButton.clickWithAssertion() } onTopBar { moreButton.clickWithAssertion() }
} }
step("Click 'Contact support' button") { step("Click 'Contact support' button") {
waitForIdle()
onDetailsScreen { contactSupportButton.clickWithAssertion() } onDetailsScreen { contactSupportButton.clickWithAssertion() }
} }
step("Assert 'Gmail' app is open") { step("Assert 'Gmail' app is open") {
@ -53,6 +56,7 @@ class FeedbackTest : BaseTestCase() {
} }
} }
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("893") @AllureId("893")
@DisplayName("Send feedback: failed transaction") @DisplayName("Send feedback: failed transaction")
@Test @Test
@ -94,17 +98,29 @@ class FeedbackTest : BaseTestCase() {
step("Enter address") { step("Enter address") {
onSendAddressScreen { addressTextField.performTextInput(recipientAddress) } onSendAddressScreen { addressTextField.performTextInput(recipientAddress) }
} }
step("Click 'Next' button") { step("Assert text field contains text: $recipientAddress") {
onSendAddressScreen { nextButton.clickWithAssertion() } onSendAddressScreen { addressTextField.assertTextEquals(recipientAddress) }
} }
step("Assert sеnding text is displayed") { step("Click 'Next' button") {
onSendConfirmScreen { sendingText.assertIsDisplayed() } onSendAddressScreen {
nextButton.clickAndWaitFor(
rule = composeTestRule,
expectedCondition = {
onSendConfirmScreen { sendingText.assertIsDisplayed() }
}
)
}
} }
step("Click 'Send' button") { step("Click 'Send' button") {
waitForIdle() waitForIdle()
onSendConfirmScreen { onSendConfirmScreen {
sendButton.assertIsEnabled() sendButton.assertIsEnabled()
sendButton.performClick() sendButton.clickAndWaitFor(
rule = composeTestRule,
expectedCondition = {
onFailedTransactionDialog { dialogContainer.assertIsDisplayed() }
}
)
} }
} }
step("Check 'Failed transaction' dialog") { step("Check 'Failed transaction' dialog") {

View file

@ -14,6 +14,7 @@ import com.tangem.tap.domain.sdk.mocks.content.*
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -85,6 +86,7 @@ class ScanCardTest : BaseTestCase() {
} }
} }
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("870") @AllureId("870")
@DisplayName("Scan: Card with Ed25519 curve") @DisplayName("Scan: Card with Ed25519 curve")
@Test @Test

View file

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

View file

@ -113,6 +113,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
val scenarioName = "user_tokens_api" val scenarioName = "user_tokens_api"
val scenarioState = "CustomTokenAdded" val scenarioState = "CustomTokenAdded"
val tokenTitle = "Myria" val tokenTitle = "Myria"
val balance = "$3,299.18"
setupHooks( setupHooks(
additionalAfterSection = { additionalAfterSection = {
resetWireMockScenarioState(scenarioName) resetWireMockScenarioState(scenarioName)
@ -134,8 +135,8 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
} }
} }
} }
step("Assert dash sign is displayed in total balance") { step("Assert correct balance is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } onMainScreen { totalBalanceText.assertTextContains(balance) }
} }
step("Assert $tokenTitle is unreachable") { step("Assert $tokenTitle is unreachable") {
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 4).assertIsUnreachable() } onMainScreen { tokenWithTitleAndPosition(tokenTitle, 4).assertIsUnreachable() }

View file

@ -126,7 +126,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) } onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
} }
step("Press 'Home' to collapse the app") { step("Press 'Home' to collapse the app") {
device.uiDevice.pressHome() collapseAppByHomeButton()
} }
step("Open the app from recent apps") { step("Open the app from recent apps") {
openTheAppFromRecents() openTheAppFromRecents()

View file

@ -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()
}
}
}
}

View file

@ -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.ENS_NAME
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_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_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_X_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
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS_WITH_TAG 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.extensions.clickWithAssertion
import com.tangem.common.utils.clearClipboard import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
@ -115,6 +117,9 @@ class SendAddressScreenTest : BaseTestCase() {
clearClipboard() clearClipboard()
} }
).run { ).run {
step("Set clipboard text") {
setClipboardText(context, recipientAddress)
}
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen() openMainScreen()
} }
@ -124,10 +129,11 @@ class SendAddressScreenTest : BaseTestCase() {
step("Open 'Send Address' screen") { step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount) openSendAddressScreen(tokenName, sendAmount)
} }
step("Set clipboard text") { step("Assert 'Addresses shimmer' is not displayed") {
setClipboardText(context, recipientAddress) onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
} }
step("Click on 'Paste' button") { step("Click on 'Paste' button") {
waitForIdle()
onSendAddressScreen { addressPasteButton.clickWithAssertion() } onSendAddressScreen { addressPasteButton.clickWithAssertion() }
} }
step("Assert address text field contains correct recipient address") { step("Assert address text field contains correct recipient address") {
@ -142,8 +148,22 @@ class SendAddressScreenTest : BaseTestCase() {
step("Set clipboard text") { step("Set clipboard text") {
setClipboardText(context, invalidAddress) setClipboardText(context, invalidAddress)
} }
step("Assert 'Addresses shimmer' is not displayed") {
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
}
step("Click on 'Cross' button") { 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") { step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() } onSendAddressScreen { addressPasteButton.clickWithAssertion() }
@ -154,20 +174,38 @@ class SendAddressScreenTest : BaseTestCase() {
step("Assert invalid address text field title is displayed") { step("Assert invalid address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) } onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
} }
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
step("Set clipboard text") { step("Set clipboard text") {
setClipboardText(context, walletAddress) 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") { 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") { step("Click on 'Paste' button") {
waitForIdle()
onSendAddressScreen { addressPasteButton.clickWithAssertion() } onSendAddressScreen { addressPasteButton.clickWithAssertion() }
} }
step("Assert address text field contains invalid address") { 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") { step("Assert 'Address is the same as wallet address' error title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) } onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) }

View file

@ -267,22 +267,21 @@ class SendConfirmScreenTest : BaseTestCase() {
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() } onSendScreen { nextButton.clickWithAssertion() }
} }
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Turn off internet") { step("Turn off internet") {
disableWiFi() disableWiFi()
disableMobileData() disableMobileData()
} }
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() } onSendAddressScreen { nextButton.clickWithAssertion() }
} }
step("Turn on internet") {
enableWiFi()
enableMobileData()
}
step("Assert 'Network fee info unreachable' warning title is displayed") { 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") { step("Assert 'Check your internet connection' warning message is displayed") {
onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() } onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() }
@ -290,6 +289,10 @@ class SendConfirmScreenTest : BaseTestCase() {
step("Assert warning icon is displayed") { step("Assert warning icon is displayed") {
onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() } onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() }
} }
step("Turn on internet") {
enableWiFi()
enableMobileData()
}
step("Click on 'Refresh' button") { step("Click on 'Refresh' button") {
waitForIdle() waitForIdle()
onSendConfirmScreen { onSendConfirmScreen {

View file

@ -17,6 +17,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -32,6 +33,7 @@ class StellarWarningsTest : BaseTestCase() {
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount) getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text) private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("4287") @AllureId("4287")
@DisplayName("Warnings: check warning, when sending less than reserve") @DisplayName("Warnings: check warning, when sending less than reserve")
@Test @Test
@ -85,6 +87,7 @@ class StellarWarningsTest : BaseTestCase() {
} }
} }
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("4286") @AllureId("4286")
@DisplayName("Warnings: check warning when sending amount equal to reserve") @DisplayName("Warnings: check warning when sending amount equal to reserve")
@Test @Test
@ -139,6 +142,7 @@ class StellarWarningsTest : BaseTestCase() {
} }
} }
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("4288") @AllureId("4288")
@DisplayName("Warnings: check warning when sending greater than reserve") @DisplayName("Warnings: check warning when sending greater than reserve")
@Test @Test

View file

@ -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() }
}
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tests.swap
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import androidx.test.InstrumentationRegistry.getTargetContext import androidx.test.InstrumentationRegistry.getTargetContext
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.assertHasBadge import com.tangem.common.extensions.assertHasBadge
import com.tangem.common.extensions.restartApp import com.tangem.common.extensions.restartApp
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
@ -208,7 +209,10 @@ class SwapStoriesTest : BaseTestCase() {
} }
step("Assert 'Swap' button has badge") { step("Assert 'Swap' button has badge") {
waitForIdle() waitForIdle()
onMainScreen { swapButton.assertHasBadge() } flakySafely(WAIT_UNTIL_TIMEOUT) {
composeTestRule.mainClock.advanceTimeBy(500)
onMainScreen { swapButton.assertHasBadge() }
}
} }
step("Open 'Swap' screen") { step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true) openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)

View file

@ -63,7 +63,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") { step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen { onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed() replaceTokensButton.assertIsDisplayed()
} }
} }
} }
@ -203,7 +203,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") { step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen { onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed() replaceTokensButton.assertIsDisplayed()
} }
} }
} }
@ -307,7 +307,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") { step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen { onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed() replaceTokensButton.assertIsDisplayed()
} }
} }
} }
@ -397,7 +397,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") { step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen { onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed() replaceTokensButton.assertIsDisplayed()
} }
} }
} }
@ -442,7 +442,7 @@ class SwapTokenScreenTest : BaseTestCase() {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
} }
step("Click on 'Swap tokens on screen' button") { step("Click on 'Swap tokens on screen' button") {
onSwapTokenScreen { swapTokensOnscreenButton.performClick() } onSwapTokenScreen { replaceTokensButton.performClick() }
waitForIdle() waitForIdle()
} }
step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") { 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") @AllureId("8536")
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)") @DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
@Test @Test

View file

@ -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.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -139,8 +138,6 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk fun getTangemHotSdk(): TangemHotSdk

View file

@ -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.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
@ -219,9 +218,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val apiConfigsManager: ApiConfigsManager private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager() get() = entryPoint.getApiConfigsManager()
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository() get() = entryPoint.getUserWalletsListRepository()
@ -385,7 +381,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
settingsManager = settingsManager, settingsManager = settingsManager,
uiMessageSender = uiMessageSender, uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository, userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk, tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy, trackingContextProxy = trackingContextProxy,

View file

@ -2,11 +2,9 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase 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.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -20,10 +18,9 @@ import kotlin.contracts.contract
class AppsFlyerReferralParamsHandler @Inject constructor( class AppsFlyerReferralParamsHandler @Inject constructor(
private val appsFlyerStore: AppsFlyerStore, private val appsFlyerStore: AppsFlyerStore,
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase, private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
dispatchers: CoroutineDispatcherProvider, private val coroutineScope: AppCoroutineScope,
) { ) {
private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob())
private val mutex = Mutex() private val mutex = Mutex()
fun handle(deepLink: DeepLink) { fun handle(deepLink: DeepLink) {

View file

@ -6,16 +6,14 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
import com.tangem.core.analytics.api.EventLogger import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder import com.tangem.core.analytics.api.UserIdHolder
import com.tangem.datasource.local.appsflyer.AppsFlyerStore 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.AppsFlyerDeepLinkListener
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -26,15 +24,13 @@ class AppsFlyerClient @AssistedInject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener, appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
tangemAFConversionListener: TangemAFConversionListener, tangemAFConversionListener: TangemAFConversionListener,
dispatchers: CoroutineDispatcherProvider,
private val appsFlyerStore: AppsFlyerStore, private val appsFlyerStore: AppsFlyerStore,
private val coroutineScope: AppCoroutineScope,
) : AppsFlyerAnalyticsClient { ) : AppsFlyerAnalyticsClient {
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
private val eventConverter = UnderscoreAnalyticsEventConverter() private val eventConverter = UnderscoreAnalyticsEventConverter()
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
init { init {
with(appsFlyerLib) { with(appsFlyerLib) {
setAppId(context.packageName) setAppId(context.packageName)

View file

@ -1,11 +1,17 @@
package com.tangem.tap.common.log package com.tangem.tap.common.log
import android.os.Build
import android.util.Log 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.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import timber.log.Timber import timber.log.Timber
import java.util.regex.Pattern
import com.orhanobut.logger.Logger as PrettyLogger
/** /**
* Tangem app logger * Tangem app logger
@ -21,32 +27,102 @@ class TangemAppLoggerInitializer(
/** Initialize */ /** Initialize */
fun initialize() { fun initialize() {
if (IS_LOG_ENABLED) { if (IS_LOG_ENABLED) {
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
} }
Timber.plant(tree = createTimberTree()) Timber.plant(tree = createTimberTree())
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
} }
private fun createTimberTree(): Timber.Tree { private fun createTimberTree(): Timber.Tree {
return object : Timber.DebugTree() { return object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (IS_LOG_ENABLED) { finalLogOutput(priority = priority, tag = tag, message = message, t = t)
Logger.log(priority, tag, message, t)
}
if (PERMITTED_PRIORITY.contains(priority)) {
appLogsStore.saveLogMessage(
tag = tag ?: "TangemAppLogger",
message = message,
)
}
} }
} }
} }
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") @Suppress("BooleanPropertyNaming")
private companion object { private companion object {
val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED
val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO) 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+)+$")
}
} }

View file

@ -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)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -21,8 +22,6 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -62,8 +61,8 @@ internal object ActivityModule {
@Provides @Provides
@Singleton @Singleton
@DelayedWork @DelayedWork
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope { fun provideActivityDelayedWorkCoroutineScope(appScope: AppCoroutineScope): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.IO) return appScope
} }
@Provides @Provides

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.card.BuildConfig import com.tangem.domain.card.BuildConfig
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
@ -17,7 +18,6 @@ import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -44,7 +44,7 @@ internal class TangemSdkManagerModule {
analyticsExceptionHandler: AnalyticsExceptionHandler, analyticsExceptionHandler: AnalyticsExceptionHandler,
blockchainToDeriveFinder: BlockchainToDeriveFinder, blockchainToDeriveFinder: BlockchainToDeriveFinder,
analyticsEventHandler: AnalyticsEventHandler, analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): TangemSdkManager { ): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) { return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources) MockTangemSdkManager(resources = context.resources)
@ -62,7 +62,7 @@ internal class TangemSdkManagerModule {
analyticsExceptionHandler = analyticsExceptionHandler, analyticsExceptionHandler = analyticsExceptionHandler,
blockchainToDeriveFinder = blockchainToDeriveFinder, blockchainToDeriveFinder = blockchainToDeriveFinder,
analyticsEventHandler = analyticsEventHandler, analyticsEventHandler = analyticsEventHandler,
dispatchers = dispatchers, coroutineScope = appScope,
) )
} }
} }

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.UiDependencies 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.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.core.ui.theme.AppThemeModeHolder
@ -30,6 +31,7 @@ internal object UiDependenciesModule {
override val vibratorHapticManager = vibratorHapticManager override val vibratorHapticManager = vibratorHapticManager
override val appThemeModeHolder = appThemeModeHolder override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler() override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
} }

View file

@ -5,10 +5,13 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener 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.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager import com.tangem.tap.common.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager import com.tangem.tap.common.share.IntentShareManager
import com.tangem.tap.common.url.CustomTabsUrlOpener import com.tangem.tap.common.url.CustomTabsUrlOpener
import com.tangem.tap.core.DefaultAppCoroutineScope
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -18,21 +21,28 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object UtilsModule { internal interface UtilsModule {
@Provides @Binds
@Singleton fun provideAppScope(defaultAppScope: DefaultAppCoroutineScope): AppCoroutineScope
fun provideShareManager(): ShareManager = IntentShareManager()
@Provides companion object {
@Singleton
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
@Provides @Provides
@Singleton @Singleton
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) fun provideShareManager(): ShareManager = IntentShareManager()
@Provides @Provides
@Singleton @Singleton
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context) fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
@Provides
@Singleton
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
@Provides
@Singleton
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager =
IntentSettingsManager(context)
}
} }

View file

@ -40,14 +40,6 @@ internal object ManageTokensDomainModule {
return FindTokenUseCase(customTokensRepository) return FindTokenUseCase(customTokensRepository)
} }
@Provides
@Singleton
fun provideCheckIsCurrencyNotAddedUseCase(
customTokensRepository: CustomTokensRepository,
): CheckIsCurrencyNotAddedUseCase {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetSupportedNetworksUseCase( fun provideGetSupportedNetworksUseCase(
@ -64,12 +56,6 @@ internal object ManageTokensDomainModule {
return ValidateDerivationPathUseCase(customTokensRepository) return ValidateDerivationPathUseCase(customTokensRepository)
} }
@Provides
@Singleton
fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase {
return CheckHasLinkedTokensUseCase(repository)
}
@Provides @Provides
@Singleton @Singleton
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase { fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {

View file

@ -8,7 +8,6 @@ import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -63,9 +62,7 @@ internal object NFTDomainModule {
fun providesGetNFTAvailableNetworksUseCase( fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleAccountListSupplier: SingleAccountListSupplier, singleAccountListSupplier: SingleAccountListSupplier,
currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase( ): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository, nftRepository = nftRepository,
singleAccountListSupplier = singleAccountListSupplier, singleAccountListSupplier = singleAccountListSupplier,
) )

View file

@ -1,9 +1,12 @@
package com.tangem.tap.di.domain 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.repository.QrScanningEventsRepository
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -31,4 +34,18 @@ internal object QrScanningDomainModule {
fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase { fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase {
return ParseQrCodeUseCase(repository) return ParseQrCodeUseCase(repository)
} }
@Provides
@Singleton
fun provideResolveQrSendTargetsUseCase(
multiAccountListSupplier: MultiAccountListSupplier,
qrScanningEventsRepository: QrScanningEventsRepository,
userWalletsListRepository: UserWalletsListRepository,
): ResolveQrSendTargetsUseCase {
return ResolveQrSendTargetsUseCase(
multiAccountListSupplier = multiAccountListSupplier,
qrScanningEventsRepository = qrScanningEventsRepository,
userWalletsListRepository = userWalletsListRepository,
)
}
} }

View file

@ -1,25 +1,20 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.exchange.RampStateManager 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.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher 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.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher 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.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher 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.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.* 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.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
@ -36,7 +31,6 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule { internal object TokensDomainModule {
@Provides @Provides
@ -53,67 +47,6 @@ internal object TokensDomainModule {
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager) 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 @Provides
@Singleton @Singleton
fun provideGetCryptoCurrencyActionsUseCase( fun provideGetCryptoCurrencyActionsUseCase(
@ -132,30 +65,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 @Provides
@Singleton @Singleton
fun provideGetMinimumTransactionAmountSyncUseCase( fun provideGetMinimumTransactionAmountSyncUseCase(
@ -239,37 +148,13 @@ internal object TokensDomainModule {
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) 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 @Provides
@Singleton @Singleton
fun provideWalletBalanceFetcher( fun provideWalletBalanceFetcher(
currenciesRepository: CurrenciesRepository, userWalletsListRepository: UserWalletsListRepository,
multiWalletCryptoCurrenciesFetcher: MultiWalletCryptoCurrenciesFetcher, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
expressServiceFetcher: ExpressServiceFetcher,
multiWalletAccountListFetcher: MultiWalletAccountListFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
@ -279,8 +164,10 @@ internal object TokensDomainModule {
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher { ): WalletBalanceFetcher {
return WalletBalanceFetcher( return WalletBalanceFetcher(
currenciesRepository = currenciesRepository, userWalletsListRepository = userWalletsListRepository,
multiWalletCryptoCurrenciesFetcher = multiWalletCryptoCurrenciesFetcher, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
expressServiceFetcher = expressServiceFetcher,
multiWalletAccountListFetcher = multiWalletAccountListFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,

View file

@ -1,16 +1,16 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.data.wallets.hot.TangemHotWalletSigner 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.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.notifications.repository.PushNotificationsRepository 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.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.GaslessTransactionRepository 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.*
import com.tangem.domain.transaction.usecase.gasless.* import com.tangem.domain.transaction.usecase.gasless.*
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
@ -56,8 +53,8 @@ internal object TransactionDomainModule {
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
dispatchers: CoroutineDispatcherProvider,
pushNotificationsRepository: PushNotificationsRepository, pushNotificationsRepository: PushNotificationsRepository,
appScope: AppCoroutineScope,
): SendTransactionUseCase { ): SendTransactionUseCase {
return SendTransactionUseCase( return SendTransactionUseCase(
demoConfig = DemoConfig, demoConfig = DemoConfig,
@ -65,7 +62,7 @@ internal object TransactionDomainModule {
transactionRepository = transactionRepository, transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.io), parallelUpdatingScope = appScope,
getHotWalletSigner = tangemHotWalletSignerFactory::create, getHotWalletSigner = tangemHotWalletSignerFactory::create,
pushNotificationsRepository = pushNotificationsRepository, pushNotificationsRepository = pushNotificationsRepository,
) )
@ -279,14 +276,12 @@ internal object TransactionDomainModule {
@Singleton @Singleton
fun provideGetAvailableFeeTokensUseCase( fun provideGetAvailableFeeTokensUseCase(
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
): GetAvailableFeeTokensUseCase { ): GetAvailableFeeTokensUseCase {
return GetAvailableFeeTokensUseCase( return GetAvailableFeeTokensUseCase(
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
) )
} }
@ -296,17 +291,15 @@ internal object TransactionDomainModule {
fun provideGetFeeForGaslessUseCase( fun provideGetFeeForGaslessUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getFeeUseCase: GetFeeUseCase, getFeeUseCase: GetFeeUseCase,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
): GetFeeForGaslessUseCase { ): GetFeeForGaslessUseCase {
return GetFeeForGaslessUseCase( return GetFeeForGaslessUseCase(
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig, demoConfig = DemoConfig,
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
currenciesRepository = currenciesRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
getFeeUseCase = getFeeUseCase, getFeeUseCase = getFeeUseCase,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
) )
@ -317,16 +310,14 @@ internal object TransactionDomainModule {
fun provideGetFeeForTokenUseCase( fun provideGetFeeForTokenUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
): GetFeeForTokenUseCase { ): GetFeeForTokenUseCase {
return GetFeeForTokenUseCase( return GetFeeForTokenUseCase(
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig, demoConfig = DemoConfig,
currenciesRepository = currenciesRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
) )
} }
@ -344,13 +335,13 @@ internal object TransactionDomainModule {
fun provideCreateAndSendGaslessTransactionUseCase( fun provideCreateAndSendGaslessTransactionUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
getSingCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, singleAccountListSupplier: SingleAccountListSupplier,
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): CreateAndSendGaslessTransactionUseCase { ): CreateAndSendGaslessTransactionUseCase {
return CreateAndSendGaslessTransactionUseCase( return CreateAndSendGaslessTransactionUseCase(
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
getSingleCryptoCurrencyStatusUseCase = getSingCryptoCurrencyStatusUseCase, singleAccountListSupplier = singleAccountListSupplier,
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository, cardSdkConfigRepository = cardSdkConfigRepository,
getHotWalletSigner = tangemHotWalletSignerFactory::create, getHotWalletSigner = tangemHotWalletSignerFactory::create,
@ -362,16 +353,14 @@ internal object TransactionDomainModule {
fun provideEstimateFeeForTokenUseCase( fun provideEstimateFeeForTokenUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
): EstimateFeeForTokenUseCase { ): EstimateFeeForTokenUseCase {
return EstimateFeeForTokenUseCase( return EstimateFeeForTokenUseCase(
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig, demoConfig = DemoConfig,
currenciesRepository = currenciesRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
) )
} }
@ -381,8 +370,7 @@ internal object TransactionDomainModule {
fun provideEstimateFeeForGaslessTxUseCase( fun provideEstimateFeeForGaslessTxUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository, gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
estimateFeeUseCase: EstimateFeeUseCase, estimateFeeUseCase: EstimateFeeUseCase,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
): EstimateFeeForGaslessTxUseCase { ): EstimateFeeForGaslessTxUseCase {
@ -390,8 +378,7 @@ internal object TransactionDomainModule {
gaslessTransactionRepository = gaslessTransactionRepository, gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig, demoConfig = DemoConfig,
currenciesRepository = currenciesRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
estimateFeeUseCase = estimateFeeUseCase, estimateFeeUseCase = estimateFeeUseCase,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
) )

View file

@ -1,9 +1,10 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
@ -15,8 +16,6 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@ -145,12 +144,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyMinAmountUseCase( fun provideYieldSupplyMinAmountUseCase(
feeRepository: FeeRepository, feeRepository: FeeRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
currenciesRepository: CurrenciesRepository, singleAccountListSupplier: SingleAccountListSupplier,
): YieldSupplyMinAmountUseCase { ): YieldSupplyMinAmountUseCase {
return YieldSupplyMinAmountUseCase( return YieldSupplyMinAmountUseCase(
feeRepository = feeRepository, feeRepository = feeRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository, singleAccountListSupplier = singleAccountListSupplier,
) )
} }
@ -159,12 +158,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyGetCurrentFeeUseCase( fun provideYieldSupplyGetCurrentFeeUseCase(
feeRepository: FeeRepository, feeRepository: FeeRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
currenciesRepository: CurrenciesRepository, singleAccountListSupplier: SingleAccountListSupplier,
): YieldSupplyGetCurrentFeeUseCase { ): YieldSupplyGetCurrentFeeUseCase {
return YieldSupplyGetCurrentFeeUseCase( return YieldSupplyGetCurrentFeeUseCase(
feeRepository = feeRepository, feeRepository = feeRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository, singleAccountListSupplier = singleAccountListSupplier,
) )
} }
@ -173,12 +172,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyGetMaxFeeUseCase( fun provideYieldSupplyGetMaxFeeUseCase(
yieldSupplyRepository: YieldSupplyRepository, yieldSupplyRepository: YieldSupplyRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
currenciesRepository: CurrenciesRepository, singleAccountListSupplier: SingleAccountListSupplier,
): YieldSupplyGetMaxFeeUseCase { ): YieldSupplyGetMaxFeeUseCase {
return YieldSupplyGetMaxFeeUseCase( return YieldSupplyGetMaxFeeUseCase(
yieldSupplyRepository = yieldSupplyRepository, yieldSupplyRepository = yieldSupplyRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository, singleAccountListSupplier = singleAccountListSupplier,
) )
} }
@ -253,12 +252,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyPendingProcessorUseCase( fun provideYieldSupplyPendingProcessorUseCase(
yieldSupplyRepository: YieldSupplyRepository, yieldSupplyRepository: YieldSupplyRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
dispatcherProvider: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): YieldSupplyPendingTracker { ): YieldSupplyPendingTracker {
return YieldSupplyPendingTracker( return YieldSupplyPendingTracker(
yieldSupplyRepository = yieldSupplyRepository, yieldSupplyRepository = yieldSupplyRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.io), coroutineScope = appScope,
) )
} }
} }

View file

@ -34,6 +34,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
@ -65,7 +66,6 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -86,7 +86,7 @@ internal class DefaultTangemSdkManager(
private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder, private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider, private val coroutineScope: AppCoroutineScope,
) : TangemSdkManager { ) : TangemSdkManager {
private val awaitInitializationMutex = Mutex() private val awaitInitializationMutex = Mutex()
@ -115,8 +115,6 @@ internal class DefaultTangemSdkManager(
override val userCodeRequestPolicy: UserCodeRequestPolicy override val userCodeRequestPolicy: UserCodeRequestPolicy
get() = tangemSdk.config.userCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io)
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean { override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
return try { return try {
needEnrollBiometrics needEnrollBiometrics

View file

@ -10,10 +10,10 @@ import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.coil.ImagePreloader 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.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.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.BalanceHidingSettings
@ -216,30 +216,28 @@ internal class MainViewModel @Inject constructor(
if (!settings.isUpdateFromToast) { if (!settings.isUpdateFromToast) {
listenToFlipsUseCase.changeUpdateEnabled(false) listenToFlipsUseCase.changeUpdateEnabled(false)
val message = BottomSheetMessage.invoke( val message = bottomSheetMessage {
iconResId = R.drawable.ic_eye_off_outline_24, infoBlock {
title = resourceReference(R.string.balance_hidden_title), title = resourceReference(R.string.balance_hidden_title)
message = resourceReference(R.string.balance_hidden_description), body = resourceReference(R.string.balance_hidden_description)
onDismissRequest = ::onBottomSheetDismissed, icon(R.drawable.ic_eye_off_outline_24)
firstActionBuilder = { }
EventMessageAction( onDismiss { onBottomSheetDismissed() }
title = resourceReference(R.string.balance_hidden_got_it_button), primaryButton {
onClick = { text = resourceReference(R.string.balance_hidden_got_it_button)
onHiddenBalanceNotificationAction(isPermanent = false) onClick = {
onDismissRequest() onHiddenBalanceNotificationAction(isPermanent = false)
}, closeBs()
) }
}, }
secondActionBuilder = { secondaryButton {
EventMessageAction( text = resourceReference(R.string.balance_hidden_do_not_show_button)
title = resourceReference(R.string.balance_hidden_do_not_show_button), onClick = {
onClick = { onHiddenBalanceNotificationAction(isPermanent = true)
onHiddenBalanceNotificationAction(isPermanent = true) closeBs()
onDismissRequest() }
}, }
) }
},
)
messageSender.send(message) messageSender.send(message)
} }
} }

View file

@ -13,7 +13,6 @@ import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
@ -71,7 +70,6 @@ data class DaggerGraphState(
val uiMessageSender: UiMessageSender? = null, val uiMessageSender: UiMessageSender? = null,
val cardArworksProvider: CardArtworksProvider? = null, val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
val userWalletsListRepository: UserWalletsListRepository? = null, val userWalletsListRepository: UserWalletsListRepository? = null,
val tangemHotSdk: TangemHotSdk? = null, val tangemHotSdk: TangemHotSdk? = null,
val trackingContextProxy: TrackingContextProxy? = null, val trackingContextProxy: TrackingContextProxy? = null,

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -28,9 +29,12 @@ import com.arkivanov.essenty.backhandler.BackHandler
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost 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.message.EventMessageEffect
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.LocalSnackbarHostState 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.res.TangemTheme
import com.tangem.core.ui.security.ProvideSecureFlagController import com.tangem.core.ui.security.ProvideSecureFlagController
import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent
@ -93,6 +97,16 @@ internal fun RootContent(
.padding(all = 16.dp), .padding(all = 16.dp),
hostState = snackbarHostState, hostState = snackbarHostState,
) )
if (LocalRedesignEnabled.current) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.padding(all = 16.dp),
hostState = LocalTopSnackbarHostState.current,
)
}
} }
} }
EventMessageEffect() EventMessageEffect()

View file

@ -349,6 +349,7 @@ internal class ChildFactory @Inject constructor(
val source = when (route.source) { val source = when (route.source) {
is AppRoute.QrScanning.Source.Send -> SourceType.SEND is AppRoute.QrScanning.Source.Send -> SourceType.SEND
is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT
is AppRoute.QrScanning.Source.MainScreen -> SourceType.MAIN_SCREEN
} }
createComponentChild( createComponentChild(
context = context, context = context,

View file

@ -5,8 +5,8 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.test.core.ProvideTestModels import com.tangem.test.core.ProvideTestModels
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import arrow.core.right import arrow.core.right
import com.tangem.common.test.TestAppCoroutineScope
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
@ -30,7 +30,7 @@ class AppsFlyerReferralParamsHandlerTest {
} }
private val handler = AppsFlyerReferralParamsHandler( private val handler = AppsFlyerReferralParamsHandler(
appsFlyerStore = appsFlyerStore, appsFlyerStore = appsFlyerStore,
dispatchers = TestingCoroutineDispatcherProvider(), coroutineScope = TestAppCoroutineScope(),
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase, setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
) )

View file

@ -13,6 +13,7 @@ dependencies {
/* Core */ /* Core */
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.configToggles) implementation(projects.core.configToggles)
implementation(projects.core.navigation)
/* Domain */ /* Domain */
implementation(projects.domain.qrScanning.models) implementation(projects.domain.qrScanning.models)

View file

@ -167,11 +167,14 @@ sealed class AppRoute(val path: String) : Route {
get() = when (this) { get() = when (this) {
is Send -> "/$networkName" is Send -> "/$networkName"
WalletConnect -> "" WalletConnect -> ""
MainScreen -> ""
} }
data class Send(val networkName: String) : Source() data class Send(val networkName: String) : Source()
data object WalletConnect : Source() data object WalletConnect : Source()
data object MainScreen : Source()
} }
} }

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -0,0 +1,323 @@
package com.tangem.common.ui.markets
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
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.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.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, TODO in [REDACTED_TASK_KEY]
)
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?, TODO in [REDACTED_TASK_KEY]
modifier: Modifier = Modifier,
) {
val ratingColor = mapRatingToColor(ratingPosition)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
TokenRatingPlace(
ratingPosition = ratingPosition,
ratingColor = ratingColor,
)
if (marketCap != null) {
TokenMarketCapText(
ratingColor = ratingColor,
modifier = Modifier.weight(1f, fill = false),
text = marketCap,
)
}
}
}
@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 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") }
}
}
}
}

View file

@ -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,
)
}
}

View file

@ -38,8 +38,8 @@ import com.tangem.utils.StringsSigns.MINUS
import kotlin.random.Random import kotlin.random.Random
@Composable @Composable
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { fun MarketsListItemV1(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
MarketsListItemContent( MarketsListItemContentV1(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RectangleShape) .clip(RectangleShape)
@ -50,7 +50,7 @@ fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onC
} }
@Composable @Composable
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) { private fun MarketsListItemContentV1(model: MarketsListItemUM, modifier: Modifier = Modifier) {
val windowSize = LocalWindowSize.current val windowSize = LocalWindowSize.current
Row( Row(
@ -273,11 +273,11 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
} }
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
MarketsListItem( MarketsListItemV1(
modifier = Modifier, modifier = Modifier,
model = state1, model = state1,
) )
MarketsListItem( MarketsListItemV1(
modifier = Modifier, modifier = Modifier,
model = state2, model = state2,
) )

View file

@ -16,7 +16,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = "", iconUrl = "",
ratingPosition = "10", ratingPosition = "1",
marketCap = "$6.233 B", marketCap = "$6.233 B",
price = MarketsListItemUM.Price(text = "31 285.72$"), price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%", trendPercentText = "12.43%",
@ -33,7 +33,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,
ratingPosition = "10", ratingPosition = "2",
marketCap = "$6.233 B", marketCap = "$6.233 B",
price = MarketsListItemUM.Price(text = "31 285.72$"), price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%", trendPercentText = "12.43%",

View file

@ -6,8 +6,12 @@ import androidx.compose.animation.core.tween
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier 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.style.TextOverflow
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
/** /**
@ -18,6 +22,23 @@ import com.tangem.core.ui.res.TangemTheme
*/ */
@Composable @Composable
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { 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 growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1 val generalColor = TangemTheme.colors.text.primary1
@ -52,4 +73,60 @@ fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType
style = TangemTheme.typography.body2, style = TangemTheme.typography.body2,
overflow = TextOverflow.Visible, 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,
)
} }

View file

@ -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.AmplitudeABTestsManager
import com.tangem.core.abtests.manager.impl.StubABTestsManager import com.tangem.core.abtests.manager.impl.StubABTestsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig 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.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -24,7 +22,7 @@ internal object ABTestsManagerModule {
fun provideABTestsManager( fun provideABTestsManager(
application: Application, application: Application,
environmentConfig: EnvironmentConfig, environmentConfig: EnvironmentConfig,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): ABTestsManager { ): ABTestsManager {
return if (BuildConfig.AB_TESTS_ENABLED) { return if (BuildConfig.AB_TESTS_ENABLED) {
StubABTestsManager() StubABTestsManager()
@ -32,7 +30,7 @@ internal object ABTestsManagerModule {
AmplitudeABTestsManager( AmplitudeABTestsManager(
application = application, application = application,
apiKey = environmentConfig.amplitudeApiKey, apiKey = environmentConfig.amplitudeApiKey,
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
) )
} }
} }

View file

@ -7,14 +7,14 @@ import com.amplitude.experiment.ExperimentConfig
import com.amplitude.experiment.ExperimentUser import com.amplitude.experiment.ExperimentUser
import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import kotlinx.coroutines.CoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
internal class AmplitudeABTestsManager( internal class AmplitudeABTestsManager(
val application: Application, val application: Application,
val apiKey: String, val apiKey: String,
val scope: CoroutineScope, val scope: AppCoroutineScope,
) : ABTestsManager { ) : ABTestsManager {
private lateinit var client: ExperimentClient private lateinit var client: ExperimentClient

View file

@ -1,6 +1,4 @@
import com.squareup.kotlinpoet.* import com.tangem.plugin.configuration.configurations.TogglesGenerator
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import org.json.JSONArray
plugins { plugins {
alias(deps.plugins.android.library) alias(deps.plugins.android.library)
@ -11,10 +9,31 @@ plugins {
id("configuration") id("configuration")
} }
buildscript { abstract class GenerateTogglesTask : DefaultTask() {
dependencies {
classpath("com.squareup:kotlinpoet:1.15.0") @get:InputFiles
classpath("org.json:json:20231013") 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") 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") { tasks.named("preBuild") {
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles) dependsOn(generateToggles)
} }
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
@ -51,77 +82,4 @@ dependencies {
testImplementation(projects.test.core) testImplementation(projects.test.core)
testRuntimeOnly(deps.test.junit5.engine) 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)
}
}
} }

View file

@ -59,5 +59,13 @@
{ {
"name": "CUSTOMER_IO_ENABLED", "name": "CUSTOMER_IO_ENABLED",
"version": "5.35" "version": "5.35"
},
{
"name": "MAIN_SCREEN_QR_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "NEW_PROMO_BANNERS_ENABLED",
"version": "undefined"
} }
] ]

View file

@ -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.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
/** /**
@ -22,7 +20,7 @@ import kotlinx.coroutines.flow.*
internal class DevApiConfigsManager( internal class DevApiConfigsManager(
private val apiConfigs: ApiConfigs, private val apiConfigs: ApiConfigs,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider, private val appScope: AppCoroutineScope,
) : MutableApiConfigsManager() { ) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>> override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
@ -51,7 +49,7 @@ internal class DevApiConfigsManager(
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments) notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
} }
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default)) .launchIn(appScope)
} }
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {

View file

@ -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.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.* 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 * Implementation of [ApiConfigsManager] in MOCK environment
@ -18,7 +30,7 @@ import kotlinx.coroutines.flow.*
*/ */
internal class MockApiConfigsManager( internal class MockApiConfigsManager(
private val apiConfigs: ApiConfigs, private val apiConfigs: ApiConfigs,
dispatchers: CoroutineDispatcherProvider, private val coroutineScope: AppCoroutineScope,
) : MutableApiConfigsManager() { ) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>> override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
@ -26,8 +38,6 @@ internal class MockApiConfigsManager(
override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true) override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true)
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
override fun initialize() = Unit override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {

View file

@ -41,7 +41,8 @@ interface TangemExpressApi {
@Query("fromNetwork") fromNetwork: String, @Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String, @Query("toContractAddress") toContractAddress: String,
@Query("toNetwork") toNetwork: String, @Query("toNetwork") toNetwork: String,
@Query("fromAmount") fromAmount: String, @Query("fromAmount") fromAmount: String?,
@Query("toAmount") toAmount: String? = null,
@Query("fromDecimals") fromDecimals: Int, @Query("fromDecimals") fromDecimals: Int,
@Query("toDecimals") toDecimals: Int, @Query("toDecimals") toDecimals: Int,
@Query("providerId") providerId: String, @Query("providerId") providerId: String,
@ -57,7 +58,8 @@ interface TangemExpressApi {
@Query("toContractAddress") toContractAddress: String, @Query("toContractAddress") toContractAddress: String,
@Query("fromAddress") fromAddress: String, @Query("fromAddress") fromAddress: String,
@Query("toNetwork") toNetwork: String, @Query("toNetwork") toNetwork: String,
@Query("fromAmount") fromAmount: String, @Query("fromAmount") fromAmount: String?,
@Query("toAmount") toAmount: String? = null,
@Query("fromDecimals") fromDecimals: Int, @Query("fromDecimals") fromDecimals: Int,
@Query("toDecimals") toDecimals: Int, @Query("toDecimals") toDecimals: Int,
@Query("providerId") providerId: String, @Query("providerId") providerId: String,

View file

@ -5,6 +5,9 @@ import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.datasource.api.promotion.models.PromoBannerV2Response import com.tangem.datasource.api.promotion.models.PromoBannerV2Response
import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.promobanners.PromoBannerDisplaysResponse
import com.tangem.datasource.api.tangemTech.models.promobanners.DismissPromoBannerRequest
import com.tangem.datasource.api.tangemTech.models.promobanners.DismissPromoBannerResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
@ -42,15 +45,6 @@ interface TangemTechApi {
@GET("v1/geo") @GET("v1/geo")
suspend fun getUserCountryCode(): GeoResponse suspend fun getUserCountryCode(): GeoResponse
@GET("v1/user-tokens/{user-id}")
suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse<UserTokensResponse>
@PUT("v1/user-tokens/{user-id}")
suspend fun saveUserTokens(
@Path(value = "user-id") userId: String,
@Body userTokens: UserTokensResponse,
): ApiResponse<Unit>
@PUT("/v1/wallets/{walletId}/tokens") @PUT("/v1/wallets/{walletId}/tokens")
suspend fun saveTokens( suspend fun saveTokens(
@Path(value = "walletId") userId: String, @Path(value = "walletId") userId: String,
@ -135,14 +129,8 @@ interface TangemTechApi {
@PATCH("v1/user-wallets/wallets/{wallet_id}") @PATCH("v1/user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit> suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@POST("v1/user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
suspend fun associateApplicationIdWithWallets(
@Path("application_id") applicationId: String,
@Body body: List<WalletIdBody>,
): ApiResponse<Unit>
@PUT("/v1/user-wallets/applications/{application_id}/wallets") @PUT("/v1/user-wallets/applications/{application_id}/wallets")
suspend fun associateApplicationIdWithWalletsV2( suspend fun associateApplicationIdWithWallets(
@Path("application_id") applicationId: String, @Path("application_id") applicationId: String,
@Body body: AssociateApplicationIdWithWalletsBody, @Body body: AssociateApplicationIdWithWalletsBody,
): ApiResponse<Unit> ): ApiResponse<Unit>
@ -197,6 +185,21 @@ interface TangemTechApi {
): ApiResponse<PromoBannerV2Response> ): ApiResponse<PromoBannerV2Response>
// endregion // endregion
// region promo banners
@GET("v1/banner/displays")
suspend fun getPromoBannerDisplays(
@Query("walletId") walletId: String,
@Query("placeholder") placeholder: String,
@Query("locale") locale: String,
): ApiResponse<PromoBannerDisplaysResponse>
@PATCH("v1/displays/{displayId}")
suspend fun dismissPromoBannerDisplay(
@Path("displayId") displayId: String,
@Body body: DismissPromoBannerRequest,
): ApiResponse<DismissPromoBannerResponse>
// endregion
/** /**
* Stores transaction hash in cache to prevent duplicate push * Stores transaction hash in cache to prevent duplicate push
* notifications for yield operations (deposit, withdraw, send). * notifications for yield operations (deposit, withdraw, send).

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models.promobanners
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DismissPromoBannerRequest(
@Json(name = "walletId") val walletId: String,
@Json(name = "isDismissed") val isDismissed: Boolean,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.tangemTech.models.promobanners
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DismissPromoBannerResponse(
@Json(name = "displayId") val displayId: String,
@Json(name = "walletId") val walletId: String,
@Json(name = "isDismissed") val isDismissed: Boolean,
@Json(name = "dismissedAt") val dismissedAt: String?,
)

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.tangemTech.models.promobanners
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PromoBannerDisplaysResponse(
@Json(name = "items") val items: List<PromoBannerDisplayDTO>,
)
@Suppress("BooleanPropertyNaming")
@JsonClass(generateAdapter = true)
data class PromoBannerDisplayDTO(
@Json(name = "id") val id: String,
@Json(name = "placeholder") val placeholder: String,
@Json(name = "priority") val priority: String,
@Json(name = "title") val title: String,
@Json(name = "subtitle") val subtitle: String,
@Json(name = "iconUrl") val iconUrl: String?,
@Json(name = "deeplink") val deeplink: String?,
@Json(name = "buttonEnabled") val buttonEnabled: Boolean,
@Json(name = "buttonText") val buttonText: String?,
@Json(name = "dismissable") val dismissable: Boolean,
)

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.di
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.datasource.local.preferences.* import com.tangem.datasource.local.preferences.*
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -20,10 +21,11 @@ internal object AppPreferencesStoreModule {
fun provideAppPreferencesStore( fun provideAppPreferencesStore(
@ApplicationContext appContext: Context, @ApplicationContext appContext: Context,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
appScope: AppCoroutineScope,
@SdkMoshi moshi: Moshi, @SdkMoshi moshi: Moshi,
): AppPreferencesStore { ): AppPreferencesStore {
return AppPreferencesStore( return AppPreferencesStore(
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io), preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, appScope = appScope),
moshi = moshi, moshi = moshi,
dispatchers = dispatchers, dispatchers = dispatchers,
) )

View file

@ -11,14 +11,12 @@ import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -30,7 +28,7 @@ internal object ExpressAssetsStoreModule {
fun provideExpressAssetsStore( fun provideExpressAssetsStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): ExpressAssetsStore { ): ExpressAssetsStore {
return DefaultExpressAssetsStore( return DefaultExpressAssetsStore(
persistenceStore = DataStoreFactory.create( persistenceStore = DataStoreFactory.create(
@ -40,7 +38,7 @@ internal object ExpressAssetsStoreModule {
defaultValue = emptyMap(), defaultValue = emptyMap(),
), ),
produceFile = { context.dataStoreFile("express_assets") }, produceFile = { context.dataStoreFile("express_assets") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
), ),
runtimeStore = RuntimeDataStore(), runtimeStore = RuntimeDataStore(),
) )

View file

@ -25,7 +25,7 @@ import com.tangem.datasource.api.visa.VisaApi
import com.tangem.datasource.di.utils.RetrofitApiBuilder import com.tangem.datasource.di.utils.RetrofitApiBuilder
import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -47,11 +47,11 @@ internal object NetworkModule {
fun provideApiConfigManager( fun provideApiConfigManager(
apiConfigs: ApiConfigs, apiConfigs: ApiConfigs,
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): ApiConfigsManager { ): ApiConfigsManager {
return when { return when {
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, dispatchers) BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, appScope)
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers) BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, appScope)
else -> ProdApiConfigsManager(apiConfigs) else -> ProdApiConfigsManager(apiConfigs)
} }
} }

View file

@ -20,14 +20,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes import com.tangem.datasource.utils.setTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -39,7 +37,7 @@ internal object StakingStoreModule {
fun provideStakingTokensStore( fun provideStakingTokensStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): StakingYieldsStore { ): StakingYieldsStore {
return DefaultStakingYieldsStore( return DefaultStakingYieldsStore(
dataStore = DataStoreFactory.create( dataStore = DataStoreFactory.create(
@ -49,7 +47,7 @@ internal object StakingStoreModule {
defaultValue = emptyList(), defaultValue = emptyList(),
), ),
produceFile = { context.dataStoreFile(fileName = "yields_cache") }, produceFile = { context.dataStoreFile(fileName = "yields_cache") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
), ),
) )
} }
@ -59,7 +57,7 @@ internal object StakingStoreModule {
fun provideYieldsBalancesPersistenceStore( fun provideYieldsBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): DataStore<Map<String, Set<YieldBalanceWrapperDTO>>> { ): DataStore<Map<String, Set<YieldBalanceWrapperDTO>>> {
return DataStoreFactory.create( return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer( serializer = MoshiDataStoreSerializer(
@ -68,7 +66,7 @@ internal object StakingStoreModule {
defaultValue = emptyMap(), defaultValue = emptyMap(),
), ),
produceFile = { context.dataStoreFile(fileName = "yield_balances") }, produceFile = { context.dataStoreFile(fileName = "yield_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
) )
} }
@ -83,7 +81,7 @@ internal object StakingStoreModule {
fun provideP2PEthPoolBalancesPersistenceStore( fun provideP2PEthPoolBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> { ): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> {
return DataStoreFactory.create( return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer( serializer = MoshiDataStoreSerializer(
@ -92,7 +90,7 @@ internal object StakingStoreModule {
defaultValue = emptyMap(), defaultValue = emptyMap(),
), ),
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") }, produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
) )
} }
@ -101,7 +99,7 @@ internal object StakingStoreModule {
fun provideP2PEthPoolVaultsStore( fun provideP2PEthPoolVaultsStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): P2PEthPoolVaultsStore { ): P2PEthPoolVaultsStore {
return DefaultP2PEthPoolVaultsStore( return DefaultP2PEthPoolVaultsStore(
dataStore = DataStoreFactory.create( dataStore = DataStoreFactory.create(
@ -111,7 +109,7 @@ internal object StakingStoreModule {
defaultValue = emptyList(), defaultValue = emptyList(),
), ),
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") }, produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
), ),
) )
} }

View file

@ -8,14 +8,12 @@ import com.tangem.datasource.local.token.DefaultTokenReceiveWarningActionStore
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.setTypes import com.tangem.datasource.utils.setTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -27,7 +25,7 @@ object TokenReceiveWarningModule {
fun provideTokenReceiveWarningStore( fun provideTokenReceiveWarningStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): TokenReceiveWarningActionStore { ): TokenReceiveWarningActionStore {
return DefaultTokenReceiveWarningActionStore( return DefaultTokenReceiveWarningActionStore(
persistenceStore = DataStoreFactory.create( persistenceStore = DataStoreFactory.create(
@ -37,7 +35,7 @@ object TokenReceiveWarningModule {
defaultValue = emptySet(), defaultValue = emptySet(),
), ),
produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") }, produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
), ),
) )
} }

View file

@ -8,16 +8,14 @@ import com.tangem.datasource.local.walletconnect.DefaultWalletConnectStore
import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.setTypes import com.tangem.datasource.utils.setTypes
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO
import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -29,9 +27,8 @@ object WalletConnectModule {
fun provideWalletConnectStore( fun provideWalletConnectStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, scope: AppCoroutineScope,
): WalletConnectStore { ): WalletConnectStore {
val scope = CoroutineScope(context = dispatchers.io + SupervisorJob())
return DefaultWalletConnectStore( return DefaultWalletConnectStore(
persistenceStore = DataStoreFactory.create( persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer( serializer = MoshiDataStoreSerializer(

View file

@ -9,14 +9,12 @@ import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -28,7 +26,7 @@ object YieldSupplyModule {
fun provideYieldMarketsStore( fun provideYieldMarketsStore(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, appScope: AppCoroutineScope,
): YieldMarketsStore { ): YieldMarketsStore {
return DefaultYieldMarketsStore( return DefaultYieldMarketsStore(
persistenceStore = DataStoreFactory.create( persistenceStore = DataStoreFactory.create(
@ -38,7 +36,7 @@ object YieldSupplyModule {
defaultValue = emptyList(), defaultValue = emptyList(),
), ),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") }, produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
), ),
) )
} }

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.di.local
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.DefaultUserTokensResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
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 LocalTokenModule {
@Provides
@Singleton
fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore {
return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.logs package com.tangem.datasource.local.logs
import android.content.Context import android.content.Context
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -27,13 +28,9 @@ import javax.inject.Singleton
class AppLogsStore @Inject constructor( class AppLogsStore @Inject constructor(
@ApplicationContext private val applicationContext: Context, @ApplicationContext private val applicationContext: Context,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val scope: AppCoroutineScope,
) { ) {
private val scope = CoroutineScope(
context = SupervisorJob() + dispatchers.io +
CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") },
)
private val mutex = Mutex() private val mutex = Mutex()
private val zipMutex = Mutex() private val zipMutex = Mutex()

View file

@ -10,12 +10,10 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.nft.custom.NFTPriceId import com.tangem.datasource.local.nft.custom.NFTPriceId
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -24,7 +22,7 @@ import javax.inject.Singleton
class NFTPersistenceStoreFactory @Inject constructor( class NFTPersistenceStoreFactory @Inject constructor(
@NetworkMoshi private val moshi: Moshi, @NetworkMoshi private val moshi: Moshi,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val dispatchers: CoroutineDispatcherProvider, private val appScope: AppCoroutineScope,
) { ) {
fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
@ -61,7 +59,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
defaultValue = defaultValue, defaultValue = defaultValue,
), ),
produceFile = { context.dataStoreFile(fileName = fileName) }, produceFile = { context.dataStoreFile(fileName = fileName) },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = appScope,
) )
private fun Network.ID.formatted(): String = rawId.value private fun Network.ID.formatted(): String = rawId.value

View file

@ -16,10 +16,8 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PR
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
import kotlinx.coroutines.CoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.SupervisorJob
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext
/** /**
* Application preferences data store 'DataStore<Preferences>'. * Application preferences data store 'DataStore<Preferences>'.
@ -35,15 +33,15 @@ internal object PreferencesDataStore {
private var INSTANCE: DataStore<Preferences>? = null private var INSTANCE: DataStore<Preferences>? = null
fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> { fun getInstance(context: Context, appScope: AppCoroutineScope): DataStore<Preferences> {
return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it } return INSTANCE ?: create(context, appScope).also { INSTANCE = it }
} }
private fun create(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> { private fun create(context: Context, appScope: AppCoroutineScope): DataStore<Preferences> {
return PreferenceDataStoreFactory.create( return PreferenceDataStoreFactory.create(
corruptionHandler = createCorruptionHandler(), corruptionHandler = createCorruptionHandler(),
migrations = createMigrations(context = context), migrations = createMigrations(context = context),
scope = CoroutineScope(context = dispatcher + SupervisorJob()), scope = appScope,
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) }, produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
) )
} }

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Store of [UserTokensResponse]
*
[REDACTED_AUTHOR]
*/
interface UserTokensResponseStore {
fun get(userWalletId: UserWalletId): Flow<UserTokensResponse?>
/** Get [UserTokensResponse] synchronously by [userWalletId] or null */
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse)
suspend fun clear(userWalletId: UserWalletId)
}

View file

@ -217,6 +217,7 @@
<string name="common_add">Hinzufügen</string> <string name="common_add">Hinzufügen</string>
<string name="common_add_to_portfolio">Zum Portfolio hinzufügen</string> <string name="common_add_to_portfolio">Zum Portfolio hinzufügen</string>
<string name="common_add_token">Token hinzufügen</string> <string name="common_add_token">Token hinzufügen</string>
<string name="common_add_tokens">Token hinzufügen</string>
<string name="common_added">Hinzugefügt</string> <string name="common_added">Hinzugefügt</string>
<string name="common_address">Vertragsadresse</string> <string name="common_address">Vertragsadresse</string>
<string name="common_all">Alle</string> <string name="common_all">Alle</string>
@ -399,6 +400,7 @@
<string name="common_unstake">Staking beenden</string> <string name="common_unstake">Staking beenden</string>
<string name="common_utxo_validate_withdrawal_message_warning">Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren.</string> <string name="common_utxo_validate_withdrawal_message_warning">Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren.</string>
<string name="common_value_copied">Wert kopiert</string> <string name="common_value_copied">Wert kopiert</string>
<string name="common_wallets">Meine Wallet</string>
<string name="common_week">Woche</string> <string name="common_week">Woche</string>
<string name="common_with">mit</string> <string name="common_with">mit</string>
<string name="common_yes">Ja</string> <string name="common_yes">Ja</string>
@ -469,9 +471,12 @@
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string> <string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>
<string name="earn_best_opportunities">Beste Gelegenheiten</string> <string name="earn_best_opportunities">Beste Gelegenheiten</string>
<string name="earn_clear_filter">Filter löschen</string> <string name="earn_clear_filter">Filter löschen</string>
<string name="earn_empty">Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein.</string>
<string name="earn_filter_all_networks">Alle Netzwerke</string> <string name="earn_filter_all_networks">Alle Netzwerke</string>
<string name="earn_filter_all_types">Alle Arten</string> <string name="earn_filter_all_types">Alle Arten</string>
<string name="earn_filter_by">Filtern nach</string> <string name="earn_filter_by">Filtern nach</string>
<string name="earn_filter_my_networks">Meine Netzwerke</string>
<string name="earn_filter_networks">Netzwerke</string>
<string name="earn_mostly_used">Meist verwendet</string> <string name="earn_mostly_used">Meist verwendet</string>
<string name="earn_no_results">Keine Ergebnisse</string> <string name="earn_no_results">Keine Ergebnisse</string>
<string name="earn_title">Verdienen</string> <string name="earn_title">Verdienen</string>
@ -826,6 +831,7 @@
<string name="markets_token_details_volume">Volumen</string> <string name="markets_token_details_volume">Volumen</string>
<string name="markets_tooltip_message">Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen</string> <string name="markets_tooltip_message">Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen</string>
<string name="markets_tooltip_title">Token hinzufügen</string> <string name="markets_tooltip_title">Token hinzufügen</string>
<string name="markets_tooltip_v2_title">Token hinzufügen</string>
<string name="markets_yield_supply_banner_description">Steiger die Leistung Deiner Assets und ermögliche Dir gleichzeitig den sofortigen Zugriff. %s</string> <string name="markets_yield_supply_banner_description">Steiger die Leistung Deiner Assets und ermögliche Dir gleichzeitig den sofortigen Zugriff. %s</string>
<string name="markets_yield_supply_banner_title">Yield-Modus aktivieren</string> <string name="markets_yield_supply_banner_title">Yield-Modus aktivieren</string>
<string name="mobile_wallet_requires_min_os_warning_body">Du musst auf die folgende Version aktualisieren: %1$s um eine mobile Wallet zu erstellen</string> <string name="mobile_wallet_requires_min_os_warning_body">Du musst auf die folgende Version aktualisieren: %1$s um eine mobile Wallet zu erstellen</string>
@ -845,6 +851,7 @@
<string name="news_related_tokens">Verwandte Token</string> <string name="news_related_tokens">Verwandte Token</string>
<string name="news_sources">Verwandte Nachrichten</string> <string name="news_sources">Verwandte Nachrichten</string>
<string name="news_stay_in_the_loop">Auf dem Laufenden bleiben</string> <string name="news_stay_in_the_loop">Auf dem Laufenden bleiben</string>
<string name="news_trending_score">Trend-Score</string>
<string name="nfc_error_unavailable">NFC ist auf deinem Gerät nicht verfügbar</string> <string name="nfc_error_unavailable">NFC ist auf deinem Gerät nicht verfügbar</string>
<string name="nft_about_title">Über NFT</string> <string name="nft_about_title">Über NFT</string>
<string name="nft_asset">NFT-Vermögenswert</string> <string name="nft_asset">NFT-Vermögenswert</string>
@ -963,12 +970,12 @@
<string name="onboarding_navbar_upgrade_wallet_biometrics">Biometrische Daten</string> <string name="onboarding_navbar_upgrade_wallet_biometrics">Biometrische Daten</string>
<string name="onboarding_seed_button_read_more">Lese mehr über die Seed-Phrase</string> <string name="onboarding_seed_button_read_more">Lese mehr über die Seed-Phrase</string>
<plurals name="onboarding_seed_generate_message_words_count"> <plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one"></item> <item quantity="one">.</item>
<item quantity="other">Schreibe diese %d-Wörter in der unten angegebenen Reihenfolge auf und bewahre sie an einem sicheren und geheimen Ort auf.</item> <item quantity="other">Schreibe diese %d-Wörter in der unten angegebenen Reihenfolge auf und bewahre sie an einem sicheren und geheimen Ort auf.</item>
</plurals> </plurals>
<string name="onboarding_seed_generate_title">Deine Seed-Phrase</string> <string name="onboarding_seed_generate_title">Deine Seed-Phrase</string>
<plurals name="onboarding_seed_generate_words_count"> <plurals name="onboarding_seed_generate_words_count">
<item quantity="one"></item> <item quantity="one">.</item>
<item quantity="other">%d Wörter</item> <item quantity="other">%d Wörter</item>
</plurals> </plurals>
<string name="onboarding_seed_import_message">Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein</string> <string name="onboarding_seed_import_message">Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein</string>
@ -1101,9 +1108,9 @@
<item quantity="one">für %d Wallet</item> <item quantity="one">für %d Wallet</item>
<item quantity="other">für %d Wallets</item> <item quantity="other">für %d Wallets</item>
</plurals> </plurals>
<string name="referral_point_currencies_description">Du bekommst ^^%1$s^^ für jede von einem Freund gekaufte Wallet auf deine %2$s Netzwerkadresse %3$s ^^30 Tage nach^^ dem</string> <string name="referral_point_currencies_description">Du erhältst ^^%1$s^^ für jede Wallet, die ein Freund kauft.\nDie Auszahlung erfolgt ^^30 Tage^^ nach dem Kauf an deine%2$s Adresse.%3$s</string>
<string name="referral_point_currencies_title">Du</string> <string name="referral_point_currencies_title">Du erhältst</string>
<string name="referral_point_discount_description_prefix">Bekommt eine</string> <string name="referral_point_discount_description_prefix">Er erhält</string>
<string name="referral_point_discount_description_suffix">beim Kauf einer Wallet auf tangem.com</string> <string name="referral_point_discount_description_suffix">beim Kauf einer Wallet auf tangem.com</string>
<string name="referral_point_discount_description_value">%s Rabatt</string> <string name="referral_point_discount_description_value">%s Rabatt</string>
<string name="referral_point_discount_title">Dein Freund</string> <string name="referral_point_discount_title">Dein Freund</string>
@ -1431,6 +1438,7 @@
<string name="swap_give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen.</string> <string name="swap_give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen.</string>
<string name="swap_promo_text">Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche.</string> <string name="swap_promo_text">Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche.</string>
<string name="swap_promo_title">Neuer Swap-Anbieter verfügbar!</string> <string name="swap_promo_title">Neuer Swap-Anbieter verfügbar!</string>
<string name="swap_search_suggestion_hint">Suchen Sie etwas anderes? Versuchen Sie es mit der Suche oder erkunden Sie eine andere Kryptowährung!</string>
<string name="swap_search_tooltip_description">Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist.</string> <string name="swap_search_tooltip_description">Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist.</string>
<string name="swap_search_tooltip_title">Nutzen Sie die Suche, um zu finden, was Sie benötigen.</string> <string name="swap_search_tooltip_title">Nutzen Sie die Suche, um zu finden, was Sie benötigen.</string>
<string name="swap_story_fifth_subtitle">Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen</string> <string name="swap_story_fifth_subtitle">Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen</string>

View file

@ -217,6 +217,7 @@
<string name="common_add">Agregar</string> <string name="common_add">Agregar</string>
<string name="common_add_to_portfolio">Añadir al portafolio</string> <string name="common_add_to_portfolio">Añadir al portafolio</string>
<string name="common_add_token">Agregar token</string> <string name="common_add_token">Agregar token</string>
<string name="common_add_tokens">Añada tokens</string>
<string name="common_added">Agregado</string> <string name="common_added">Agregado</string>
<string name="common_address">Dirección</string> <string name="common_address">Dirección</string>
<string name="common_all">Todos</string> <string name="common_all">Todos</string>

View file

@ -217,6 +217,7 @@
<string name="common_add">Ajouter</string> <string name="common_add">Ajouter</string>
<string name="common_add_to_portfolio">Ajouter au portfolio</string> <string name="common_add_to_portfolio">Ajouter au portfolio</string>
<string name="common_add_token">Ajouter un jeton</string> <string name="common_add_token">Ajouter un jeton</string>
<string name="common_add_tokens">Ajouter des jetons</string>
<string name="common_added">Ajouté</string> <string name="common_added">Ajouté</string>
<string name="common_address">Adresse</string> <string name="common_address">Adresse</string>
<string name="common_all">Tous</string> <string name="common_all">Tous</string>

View file

@ -214,6 +214,7 @@
<string name="common_add">追加</string> <string name="common_add">追加</string>
<string name="common_add_to_portfolio">ポートフォリオに追加</string> <string name="common_add_to_portfolio">ポートフォリオに追加</string>
<string name="common_add_token">トークンを追加</string> <string name="common_add_token">トークンを追加</string>
<string name="common_add_tokens">トークンの追加</string>
<string name="common_added">追加済み</string> <string name="common_added">追加済み</string>
<string name="common_address">アドレス</string> <string name="common_address">アドレス</string>
<string name="common_all">すべて</string> <string name="common_all">すべて</string>
@ -692,6 +693,7 @@
<string name="koinos_mana_level_title">Manaレベル</string> <string name="koinos_mana_level_title">Manaレベル</string>
<string name="main_empty_tokens_list_message">暗号資産および取引の追跡を開始するには、トークンを追加してください</string> <string name="main_empty_tokens_list_message">暗号資産および取引の追跡を開始するには、トークンを追加してください</string>
<string name="main_manage_tokens">トークンの管理</string> <string name="main_manage_tokens">トークンの管理</string>
<string name="main_qr_scan_hint">QRコードをスキャンして送金するか、アプリに接続します。</string>
<string name="main_scan_card_warning_view_subtitle">すべてのネットワークにアクセスするには、カードをスキャンする必要があります。</string> <string name="main_scan_card_warning_view_subtitle">すべてのネットワークにアクセスするには、カードをスキャンする必要があります。</string>
<string name="main_scan_card_warning_view_title">カードまたはリングをスキャンする</string> <string name="main_scan_card_warning_view_title">カードまたはリングをスキャンする</string>
<string name="main_swap_changelly_promotion_message">2月%2$s - %3$sの期間、Changelly経由のスワップは%1$sのサービス手数料となります。</string> <string name="main_swap_changelly_promotion_message">2月%2$s - %3$sの期間、Changelly経由のスワップは%1$sのサービス手数料となります。</string>
@ -839,6 +841,7 @@
<string name="news_related_tokens">関連トークン</string> <string name="news_related_tokens">関連トークン</string>
<string name="news_sources">関連ニュース</string> <string name="news_sources">関連ニュース</string>
<string name="news_stay_in_the_loop">最新情報を入手</string> <string name="news_stay_in_the_loop">最新情報を入手</string>
<string name="news_trending_score">トレンドスコア</string>
<string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string> <string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string>
<string name="nft_about_title">NFTについて</string> <string name="nft_about_title">NFTについて</string>
<string name="nft_asset">NFTアセット</string> <string name="nft_asset">NFTアセット</string>
@ -1488,6 +1491,7 @@
<string name="tangempay_cancel_kyc">メイン画面からKYCを非表示にする</string> <string name="tangempay_cancel_kyc">メイン画面からKYCを非表示にする</string>
<string name="tangempay_card_details_add_funds">資金を追加</string> <string name="tangempay_card_details_add_funds">資金を追加</string>
<string name="tangempay_card_details_add_funds_subtitle">入金オプション</string> <string name="tangempay_card_details_add_funds_subtitle">入金オプション</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Googleウォレットに追加</string>
<string name="tangempay_card_details_card_number">カード番号</string> <string name="tangempay_card_details_card_number">カード番号</string>
<string name="tangempay_card_details_change_pin">PINを変更する</string> <string name="tangempay_card_details_change_pin">PINを変更する</string>
<string name="tangempay_card_details_change_pin_success_description">カードは支払いの準備が整いました。</string> <string name="tangempay_card_details_change_pin_success_description">カードは支払いの準備が整いました。</string>
@ -1876,6 +1880,8 @@
<string name="warning_express_refresh_required_title">サービスは一時的に利用できません</string> <string name="warning_express_refresh_required_title">サービスは一時的に利用できません</string>
<string name="warning_express_too_maximum_amount_title">スワップするトークンの量は %s を超えないでください</string> <string name="warning_express_too_maximum_amount_title">スワップするトークンの量は %s を超えないでください</string>
<string name="warning_express_too_minimal_amount_title">スワップ金額は %s 以上である必要があります</string> <string name="warning_express_too_minimal_amount_title">スワップ金額は %s 以上である必要があります</string>
<string name="warning_express_unsupported_pair_description">このペアではスワップを利用できません。別のトークンを選択して、もう一度お試しください。</string>
<string name="warning_express_unsupported_pair_title">スワップ非対応のペアです</string>
<string name="warning_express_wrong_amount_description">スワップの金額を変更してください</string> <string name="warning_express_wrong_amount_description">スワップの金額を変更してください</string>
<string name="warning_failed_to_verify_card_message">このカードは、サンプル品または偽造品である可能性があります</string> <string name="warning_failed_to_verify_card_message">このカードは、サンプル品または偽造品である可能性があります</string>
<string name="warning_failed_to_verify_card_title">真正性チェックに失敗しました</string> <string name="warning_failed_to_verify_card_title">真正性チェックに失敗しました</string>

View file

@ -223,6 +223,7 @@
<string name="common_add">Добавить</string> <string name="common_add">Добавить</string>
<string name="common_add_to_portfolio">Добавить в портфель</string> <string name="common_add_to_portfolio">Добавить в портфель</string>
<string name="common_add_token">Добавить токен</string> <string name="common_add_token">Добавить токен</string>
<string name="common_add_tokens">Добавьте токены</string>
<string name="common_added">Добавлен</string> <string name="common_added">Добавлен</string>
<string name="common_address">Адрес</string> <string name="common_address">Адрес</string>
<string name="common_all">Все</string> <string name="common_all">Все</string>
@ -408,6 +409,7 @@
<string name="common_understand">Я понял</string> <string name="common_understand">Я понял</string>
<string name="common_understand_continue">Я понимаю, продолжить</string> <string name="common_understand_continue">Я понимаю, продолжить</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string> <string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unlock">Разблокировать</string>
<string name="common_unreachable">Недоступно</string> <string name="common_unreachable">Недоступно</string>
<string name="common_unstake">Завершить стейкинг</string> <string name="common_unstake">Завершить стейкинг</string>
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string> <string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
@ -733,13 +735,13 @@
<string name="manage_tokens_remove">Удалить</string> <string name="manage_tokens_remove">Удалить</string>
<string name="manage_tokens_search_placeholder">Например Bitcoin</string> <string name="manage_tokens_search_placeholder">Например Bitcoin</string>
<string name="manage_tokens_toast_portfolio_updated">Ваш портфель был обновлен</string> <string name="manage_tokens_toast_portfolio_updated">Ваш портфель был обновлен</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string> <string name="manage_tokens_unavailable_description">Выбранный токен недоступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string> <string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string> <string name="manage_tokens_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string>
<string name="markets_about_coin_header">О монете</string> <string name="markets_about_coin_header">О монете</string>
<string name="markets_add_to_my_portfolio_description">Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель</string> <string name="markets_add_to_my_portfolio_description">Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Этот актив в настоящее время не поддерживается в кошельке</string> <string name="markets_add_to_my_portfolio_unavailable_description">Этот актив в настоящее время не поддерживается в кошельке</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Этот токен не доступен для данного кошелька</string> <string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Этот токен недоступен для данного кошелька</string>
<string name="markets_add_token">Добавить</string> <string name="markets_add_token">Добавить</string>
<string name="markets_apy_placeholder">APY %s</string> <string name="markets_apy_placeholder">APY %s</string>
<string name="markets_common_my_portfolio">Мой портфель</string> <string name="markets_common_my_portfolio">Мой портфель</string>
@ -843,6 +845,7 @@
<string name="markets_token_details_trading_volume">Объем торгов (24ч)</string> <string name="markets_token_details_trading_volume">Объем торгов (24ч)</string>
<string name="markets_token_details_trading_volume_24h_description">Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке.</string> <string name="markets_token_details_trading_volume_24h_description">Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке.</string>
<string name="markets_token_details_trading_volume_full">Объем торгов (24ч)</string> <string name="markets_token_details_trading_volume_full">Объем торгов (24ч)</string>
<string name="markets_token_details_valuation_value_in_total">%s в сумме</string>
<string name="markets_token_details_volume">Объем</string> <string name="markets_token_details_volume">Объем</string>
<string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string> <string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string>
<string name="markets_tooltip_title">Добавить токены</string> <string name="markets_tooltip_title">Добавить токены</string>
@ -1499,7 +1502,7 @@
<string name="swapping_swap_action_in_progress">Обмен…</string> <string name="swapping_swap_action_in_progress">Обмен…</string>
<string name="swapping_to_title">Вы получите</string> <string name="swapping_to_title">Вы получите</string>
<string name="swapping_token_list_title">Выберите токен</string> <string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_not_available">не доступен</string> <string name="swapping_token_not_available">недоступен</string>
<string name="tangem_pay_beta_notification_subtitle">Будем рады вашей обратной связи</string> <string name="tangem_pay_beta_notification_subtitle">Будем рады вашей обратной связи</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay в режиме beta</string> <string name="tangem_pay_beta_notification_title">Tangem Pay в режиме beta</string>
<string name="tangem_pay_card_frozen">Карта заморожена</string> <string name="tangem_pay_card_frozen">Карта заморожена</string>

View file

@ -223,6 +223,7 @@
<string name="common_add">Додати</string> <string name="common_add">Додати</string>
<string name="common_add_to_portfolio">Додати у портфель</string> <string name="common_add_to_portfolio">Додати у портфель</string>
<string name="common_add_token">Додати токен</string> <string name="common_add_token">Додати токен</string>
<string name="common_add_tokens">Додайте токени</string>
<string name="common_added">Додано</string> <string name="common_added">Додано</string>
<string name="common_address">Адреса</string> <string name="common_address">Адреса</string>
<string name="common_all">Усе</string> <string name="common_all">Усе</string>

View file

@ -217,6 +217,7 @@
<string name="common_add">Add</string> <string name="common_add">Add</string>
<string name="common_add_to_portfolio">Add to portfolio</string> <string name="common_add_to_portfolio">Add to portfolio</string>
<string name="common_add_token">Add token</string> <string name="common_add_token">Add token</string>
<string name="common_add_tokens">Add tokens</string>
<string name="common_added">Added</string> <string name="common_added">Added</string>
<string name="common_address">Address</string> <string name="common_address">Address</string>
<string name="common_all">All</string> <string name="common_all">All</string>
@ -325,6 +326,7 @@
<string name="common_nft">NFT</string> <string name="common_nft">NFT</string>
<string name="common_no">No</string> <string name="common_no">No</string>
<string name="common_no_address">No address</string> <string name="common_no_address">No address</string>
<string name="common_no_results">No results</string>
<string name="common_not_added">Not Added</string> <string name="common_not_added">Not Added</string>
<string name="common_not_available">Not available</string> <string name="common_not_available">Not available</string>
<string name="common_not_now">Not now</string> <string name="common_not_now">Not now</string>
@ -383,6 +385,7 @@
<string name="common_to">To</string> <string name="common_to">To</string>
<string name="common_to_wallet_name">To %s</string> <string name="common_to_wallet_name">To %s</string>
<string name="common_today">Today</string> <string name="common_today">Today</string>
<string name="common_token_send">Token to send</string>
<plurals name="common_tokens_count"> <plurals name="common_tokens_count">
<item quantity="one">%d token</item> <item quantity="one">%d token</item>
<item quantity="other">%d tokens</item> <item quantity="other">%d tokens</item>
@ -395,6 +398,7 @@
<string name="common_understand">I understand</string> <string name="common_understand">I understand</string>
<string name="common_understand_continue">I understand, continue</string> <string name="common_understand_continue">I understand, continue</string>
<string name="common_unknown_error">There was an error. Please try again.</string> <string name="common_unknown_error">There was an error. Please try again.</string>
<string name="common_unlock">Unlock</string>
<string name="common_unreachable">Unreachable</string> <string name="common_unreachable">Unreachable</string>
<string name="common_unstake">Unstake</string> <string name="common_unstake">Unstake</string>
<string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string> <string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
@ -699,6 +703,7 @@
<string name="koinos_mana_level_title">Mana level</string> <string name="koinos_mana_level_title">Mana level</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string> <string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
<string name="main_manage_tokens">Manage tokens</string> <string name="main_manage_tokens">Manage tokens</string>
<string name="main_qr_scan_hint">Scan QR code to send funds or connect to an app</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string> <string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
<string name="main_scan_card_warning_view_title">Scan your card or ring</string> <string name="main_scan_card_warning_view_title">Scan your card or ring</string>
<string name="main_swap_changelly_promotion_message">Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s</string> <string name="main_swap_changelly_promotion_message">Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s</string>
@ -736,6 +741,8 @@
<string name="markets_earn_common_title">Earn with Tangem</string> <string name="markets_earn_common_title">Earn with Tangem</string>
<string name="markets_generate_addresses_notification">To generate addresses for selected networks, you must scan your Tangem Wallet card or ring</string> <string name="markets_generate_addresses_notification">To generate addresses for selected networks, you must scan your Tangem Wallet card or ring</string>
<string name="markets_hint">To add tokens pull this up or tap the search bar</string> <string name="markets_hint">To add tokens pull this up or tap the search bar</string>
<string name="markets_hint_part_one">Swipe up to explore the market</string>
<string name="markets_hint_part_two">Find new hidden gems</string>
<string name="markets_insights_info_description_message">This sections data is sourced from the following networks: %s</string> <string name="markets_insights_info_description_message">This sections data is sourced from the following networks: %s</string>
<string name="markets_loading_error_title">Unable to load the data…</string> <string name="markets_loading_error_title">Unable to load the data…</string>
<string name="markets_loading_no_data_title">No data</string> <string name="markets_loading_no_data_title">No data</string>
@ -798,6 +805,10 @@
<string name="markets_token_details_holders">Holders</string> <string name="markets_token_details_holders">Holders</string>
<string name="markets_token_details_holders_description">The change in the number of token holders within a specific timeframe</string> <string name="markets_token_details_holders_description">The change in the number of token holders within a specific timeframe</string>
<string name="markets_token_details_holders_full">Holders</string> <string name="markets_token_details_holders_full">Holders</string>
<string name="markets_token_details_insight_day_timeline">D</string>
<string name="markets_token_details_insight_month_timeline">M</string>
<string name="markets_token_details_insight_week_timeline">W</string>
<string name="markets_token_details_insight_year_timeline">Y</string>
<string name="markets_token_details_insights">Insights</string> <string name="markets_token_details_insights">Insights</string>
<string name="markets_token_details_links">Links</string> <string name="markets_token_details_links">Links</string>
<string name="markets_token_details_liquidity">Liquidity</string> <string name="markets_token_details_liquidity">Liquidity</string>
@ -825,9 +836,11 @@
<string name="markets_token_details_total_supply">Total supply</string> <string name="markets_token_details_total_supply">Total supply</string>
<string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string> <string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
<string name="markets_token_details_total_supply_full">Total supply</string> <string name="markets_token_details_total_supply_full">Total supply</string>
<string name="markets_token_details_trading_interval">24h</string>
<string name="markets_token_details_trading_volume">Trading volume (24h)</string> <string name="markets_token_details_trading_volume">Trading volume (24h)</string>
<string name="markets_token_details_trading_volume_24h_description">The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market</string> <string name="markets_token_details_trading_volume_24h_description">The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market</string>
<string name="markets_token_details_trading_volume_full">Trading volume (24h)</string> <string name="markets_token_details_trading_volume_full">Trading volume (24h)</string>
<string name="markets_token_details_valuation_value_in_total">%s in total</string>
<string name="markets_token_details_volume">Volume</string> <string name="markets_token_details_volume">Volume</string>
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string> <string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
<string name="markets_tooltip_title">Add tokens</string> <string name="markets_tooltip_title">Add tokens</string>
@ -1202,6 +1215,10 @@
<string name="send_memo">Memo: %s</string> <string name="send_memo">Memo: %s</string>
<string name="send_memo_destination_tag_error">Invalid Memo</string> <string name="send_memo_destination_tag_error">Invalid Memo</string>
<string name="send_network_fee_warning_title">Network fee coverage</string> <string name="send_network_fee_warning_title">Network fee coverage</string>
<plurals name="send_network_selection_hidden_tokens">
<item quantity="one">%d token isn\'t compatible with this address</item>
<item quantity="other">%d tokens aren\'t compatible with this address</item>
</plurals>
<string name="send_nonce">Nonce</string> <string name="send_nonce">Nonce</string>
<string name="send_nonce_footer">Unique number for each transaction. Use it to resend or cancel a pending transaction.</string> <string name="send_nonce_footer">Unique number for each transaction. Use it to resend or cancel a pending transaction.</string>
<string name="send_nonce_hint">Enter nonce…</string> <string name="send_nonce_hint">Enter nonce…</string>
@ -1509,6 +1526,7 @@
<string name="tangempay_cancel_kyc">Hide KYC from main screen</string> <string name="tangempay_cancel_kyc">Hide KYC from main screen</string>
<string name="tangempay_card_details_add_funds">Add funds</string> <string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string> <string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Add to Google Wallet</string>
<string name="tangempay_card_details_card_number">Card Number</string> <string name="tangempay_card_details_card_number">Card Number</string>
<string name="tangempay_card_details_change_pin">Change PIN</string> <string name="tangempay_card_details_change_pin">Change PIN</string>
<string name="tangempay_card_details_change_pin_success_description">The card is fully ready for payments.</string> <string name="tangempay_card_details_change_pin_success_description">The card is fully ready for payments.</string>

View file

@ -24,7 +24,7 @@ dependencies {
/** Project - Core */ /** Project - Core */
implementation(projects.core.res) implementation(projects.core.res)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.core.decompose) api(projects.core.decompose)
implementation(projects.core.error) implementation(projects.core.error)
/** AndroidX libraries */ /** AndroidX libraries */

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.core.ui.theme.AppThemeModeHolder
@ -15,6 +16,8 @@ interface UiDependencies {
val globalSnackbarHostState: SnackbarHostState val globalSnackbarHostState: SnackbarHostState
val globalTopSnackbarHostState: TangemTopSnackbarHostState
val eventMessageHandler: EventMessageHandler val eventMessageHandler: EventMessageHandler
val designFeatureToggles: DesignFeatureToggles val designFeatureToggles: DesignFeatureToggles

View file

@ -39,22 +39,18 @@ fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemThe
} }
/** /**
* A composable that draws a fade effect at the right end of the screen. Same as [BottomFade] * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
* but with a horizontal gradient. * elements and floating button at the bottom of the screen.
*/ */
@Composable @Composable
fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Box( Box(
modifier = modifier modifier = modifier
.fillMaxHeight() .fillMaxWidth()
.background( .height(TangemTheme.dimens.size100 + bottomBarHeight)
brush = Brush.horizontalGradient( .background(gradientBrush),
colors = listOf(
Color.Transparent,
backgroundColor,
),
),
),
) )
} }
@ -84,8 +80,11 @@ fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
), ),
) { ) {
progressive = progressive = HazeProgressive.verticalGradient(
HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f) startIntensity = 0f,
endIntensity = 1f,
preferPerformance = true,
)
}, },
) )
} }
@ -114,7 +113,11 @@ fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier
), ),
) { ) {
progressive = progressive =
HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f) HazeProgressive.horizontalGradient(
startIntensity = 0f,
endIntensity = 1f,
preferPerformance = true,
)
}, },
) )
} }

View file

@ -16,16 +16,18 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import com.tangem.core.ui.res.LocalIsInDarkTheme
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) { internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "FluidMeshGradient") val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
val isDark = LocalIsInDarkTheme.current
// ── Circle 1 (left) ────────────────────────────────────────────────────── // ── Circle 1 (left) — matches dc1 / lc1 from shader version ─────────────
val color1 by transition.animateColor( val color1 by transition.animateColor(
initialValue = Color(0xFF3355EE), initialValue = if (isDark) Color(0xFF0D0D3A) else Color(0xFFCCB8EE),
targetValue = Color(0xFF5577FF), targetValue = if (isDark) Color(0xFF1C1C6E) else Color(0xFFBBA0E8),
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(4_000, easing = FastOutSlowInEasing), animation = tween(4_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse, repeatMode = RepeatMode.Reverse,
@ -51,10 +53,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
label = "y1", label = "y1",
) )
// ── Circle 2 (right) ───────────────────────────────────────────────────── // ── Circle 2 (right) — matches dc2 / lc2 from shader version ────────────
val color2 by transition.animateColor( val color2 by transition.animateColor(
initialValue = Color(0xFF7733CC), initialValue = if (isDark) Color(0xFF0A1238) else Color(0xFFB8C8F0),
targetValue = Color(0xFF4455EE), targetValue = if (isDark) Color(0xFF112266) else Color(0xFF9AAEE8),
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(5_000, easing = FastOutSlowInEasing), animation = tween(5_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse, repeatMode = RepeatMode.Reverse,
@ -82,10 +84,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
label = "y2", label = "y2",
) )
// ── Oval (center) ──────────────────────────────────────────────────────── // ── Oval (center) — matches dc4 / lc4 from shader version ───────────────
val ovalColor by transition.animateColor( val ovalColor by transition.animateColor(
initialValue = Color(0xFF5533CC), initialValue = if (isDark) Color(0xFF081A30) else Color(0xFFB8C4EE),
targetValue = Color(0xFF8844EE), targetValue = if (isDark) Color(0xFF113355) else Color(0xFFA8B4E8),
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(7_000, easing = FastOutSlowInEasing), animation = tween(7_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse, repeatMode = RepeatMode.Reverse,
@ -93,10 +95,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
), ),
label = "ovalColor", label = "ovalColor",
) )
// ── Circle 3 (center) ──────────────────────────────────────────────────── // ── Circle 3 (center) — matches dc3 / lc3 from shader version ───────────
val color3 by transition.animateColor( val color3 by transition.animateColor(
initialValue = Color(0xFF9933BB), initialValue = if (isDark) Color(0xFF110A38) else Color(0xFFDDC8F5),
targetValue = Color(0xFFBB44DD), targetValue = if (isDark) Color(0xFF2A1666) else Color(0xFFCCB0EE),
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(6_000, easing = FastOutSlowInEasing), animation = tween(6_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse, repeatMode = RepeatMode.Reverse,

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.background.shaderBackground import com.tangem.core.ui.components.background.shaderBackground
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.LocalPowerSavingState import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
@ -36,99 +37,58 @@ fun NorthernLightsBackground(
} }
} }
@Suppress("LongMethod") @Suppress("LongMethod", "NamedArguments")
@Composable @Composable
private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) { private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2") val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
val isDark = LocalIsInDarkTheme.current
// Each track cycles through 4 states (matching the screenshot frames): @Composable
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back fun anim(a: Color, b: Color, c: Color, d: Color, offset: Int = 0) = transition.animateColor(
// 16 s total per track, staggered so no two tracks peak simultaneously. initialValue = a,
targetValue = a,
// ── Color 1 indigo → bright blue → lavender → hot violet ──────────────
val color1 by transition.animateColor(
initialValue = Color(0xFF2A1480),
targetValue = Color(0xFF2A1480),
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = keyframes { animation = keyframes {
durationMillis = 16_000 durationMillis = 40_000
Color(0xFF2A1480) at 0 using FastOutSlowInEasing a at 0 using FastOutSlowInEasing
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing b at 10_000 using FastOutSlowInEasing
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing c at 20_000 using FastOutSlowInEasing
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing d at 30_000 using FastOutSlowInEasing
}, },
repeatMode = RepeatMode.Restart, repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(offset),
), ),
label = "color1", label = "c$offset",
) )
// ── Color 2 dark blue → cyan-blue → sky → teal ───────────────────────── val dc1 by anim(Color(0xFF0D0D3A), Color(0xFF141455), Color(0xFF1C1C6E), Color(0xFF111148), 0)
val color2 by transition.animateColor( val dc2 by anim(Color(0xFF0A1238), Color(0xFF0D1D55), Color(0xFF112266), Color(0xFF0E1A4A), 10_000)
initialValue = Color(0xFF1444AA), val dc3 by anim(Color(0xFF110A38), Color(0xFF1C1050), Color(0xFF2A1666), Color(0xFF180E48), 20_000)
targetValue = Color(0xFF1444AA), val dc4 by anim(Color(0xFF081A30), Color(0xFF0D2844), Color(0xFF113355), Color(0xFF0D2240), 5_000)
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF1444AA) at 0 using FastOutSlowInEasing
Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing
Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing
Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(4_000),
),
label = "color2",
)
// ── Color 3 dark purple → medium purple → rose pink → magenta ────────── val lc1 by anim(Color(0xFFCCB8EE), Color(0xFFBBA0E8), Color(0xFFCCB0F0), Color(0xFFC4AAEC), 0)
val color3 by transition.animateColor( val lc2 by anim(Color(0xFFB8C8F0), Color(0xFF9AAEE8), Color(0xFFAABEF0), Color(0xFFA0B8EE), 10_000)
initialValue = Color(0xFF4422BB), val lc3 by anim(Color(0xFFDDC8F5), Color(0xFFCCB0EE), Color(0xFFD8BEF5), Color(0xFFD0B8F2), 20_000)
targetValue = Color(0xFF4422BB), val lc4 by anim(Color(0xFFB8C4EE), Color(0xFFA8B4E8), Color(0xFFB4C0EE), Color(0xFFAABCEC), 5_000)
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF4422BB) at 0 using FastOutSlowInEasing
Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing
Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing
Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(8_000),
),
label = "color3",
)
// ── Color 4 dark violet → medium violet → light pink → hot pink ──────── val color1 = if (isDark) dc1 else lc1
val color4 by transition.animateColor( val color2 = if (isDark) dc2 else lc2
initialValue = Color(0xFF331199), val color3 = if (isDark) dc3 else lc3
targetValue = Color(0xFF331199), val color4 = if (isDark) dc4 else lc4
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF331199) at 0 using FastOutSlowInEasing
Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing
Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing
Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(2_000),
),
label = "color4",
)
// Keep a stable shader instance so the RuntimeShader is never recreated. // Keep a stable shader instance so the RuntimeShader is never recreated.
// Colors are pushed each recomposition via updateColors(). // Colors are pushed each recomposition via updateColors().
val shader = remember { val shader = remember {
NorthernLightsMeshGradientShader( NorthernLightsMeshGradientShader(
colors = arrayOf( colors = arrayOf(
Color(0xFF2A1480), color1,
Color(0xFF1444AA), color2,
Color(0xFF4422BB), color3,
Color(0xFF331199), color4,
containerColor, containerColor,
), ),
speed = 0.5f, speed = 0.07f,
scale = 4f, scale = 1.8f,
) )
} }
val colorsArray = remember { Array(5) { Color.Unspecified } } val colorsArray = remember { Array(5) { Color.Unspecified } }

View file

@ -40,7 +40,7 @@ inline fun <T : Any> InformationBlockContentScope.ListItems(
} }
@Composable @Composable
inline fun <T : Any> InformationBlockContentScope.GridItems( inline fun <T : Any> GridItems(
items: ImmutableList<T>, items: ImmutableList<T>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
verticalAlignment: Alignment.Vertical = Alignment.Top, verticalAlignment: Alignment.Vertical = Alignment.Top,

View file

@ -0,0 +1,430 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType.Default
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType.Modal
import com.tangem.core.ui.components.bottomsheets.internal.InternalBottomSheet
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.components.bottomsheets.modal.MODAL_SHEET_MAX_HEIGHT
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
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.utils.WindowInsetsZero
/**
* Type of [TangemBottomSheet] that defines its behavior and appearance.
* - [Default]: Standard bottom sheet with a draggable header
* - [Modal]: Modal bottom sheet without a draggable header
*/
enum class TangemBottomSheetType {
Default, Modal;
fun getDragHandle(): (@Composable (() -> Unit))? = when (this) {
Default -> {
{ TangemBottomSheetDraggableHeader() }
}
Modal -> null
}
}
/**
* Modal bottom sheet with [content] and optional [title] and [footer].
*
* @param config Configuration for the bottom sheet, including visibility and content data.
* @param type Type of the bottom sheet that defines its behavior and appearance.
* @param containerColor Background color of the bottom sheet container.
* @param skipPartiallyExpanded Whether to skip the partially expanded state when dragging the sheet.
* @param onBack Optional callback for handling back press when the sheet is visible.
* @param title Optional composable for rendering the title section of the sheet, receiving the content
* model as a parameter.
* @param content Composable for rendering the main content of the sheet, receiving the content model
* as a parameter.
* @param footer Optional composable for rendering the footer section of the sheet, receiving the content
* model as a parameter.
*
* [Show in Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8454-23749&m=dev)
*/
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
type: TangemBottomSheetType = Default,
containerColor: Color = TangemTheme.colors2.surface.level2,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)? = null,
) {
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
if (isAlwaysVisible) {
PreviewModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@Suppress("LongParameterList")
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
type: TangemBottomSheetType,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = if (config.dismissOnClickOutside == null) {
rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
} else {
rememberModalBottomSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = { sheetValue ->
if (config.dismissOnClickOutside().not()) {
// Ignore transitions to hidden (prevents dismiss on outside click/back press)
sheetValue != SheetValue.Hidden
} else {
true
}
},
)
}
if (isVisible && config.content is T) {
BasicBottomSheet<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
type = type,
title = title,
onBack = onBack,
content = content,
footer = footer,
)
}
LaunchedEffect(key1 = config.isShown) {
if (config.isShown) {
isVisible = true
} else {
sheetState.collapse { isVisible = false }
}
}
}
@Suppress("LongParameterList")
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
type: TangemBottomSheetType,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
BasicBottomSheet<T>(
config = config,
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
)
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
type: TangemBottomSheetType,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
val model = config.content as? T ?: return
val windowSize = LocalWindowSize.current
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = when (type) {
Default -> Dp.Unspecified
Modal -> windowSize.height * MODAL_SHEET_MAX_HEIGHT
}
val contentModifier = when (type) {
Default -> Modifier.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
),
)
Modal -> Modifier
.padding(
start = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x2,
bottom = TangemTheme.dimens2.x2,
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
}
Column(
modifier = contentModifier
.background(containerColor)
.heightIn(max = maxHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Box(modifier = Modifier.fillMaxWidth()) {
content(model)
if (footer != null) {
BottomFade(
modifier = Modifier.align(Alignment.BottomCenter),
gradientBrush = Brush.verticalGradient(
listOf(
TangemTheme.colors2.shadow.fadeMin,
TangemTheme.colors2.shadow.fadeMax,
),
),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
) {
if (footer != null) {
footer(model)
}
}
}
}
}
InternalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = type.getDragHandle(),
content = bsContent,
scrimColor = TangemTheme.colors2.overlay.overlaySecondary,
)
}
// region Preview
@Suppress("LongMethod")
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemModalBottomSheetWithFooter_Preview(
@PreviewParameter(TangemBottomSheetPreviewProvider::class) params: TangemBottomSheetType,
) {
TangemThemePreviewRedesign {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
type = params,
title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) },
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
Text(
text = "Long text to show scrollable content and bottom fade." +
"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " +
"ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " +
"consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " +
"dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " +
"habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
"Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " +
"pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " +
"turpis pellentesque rhoncus.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
},
footer = {
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "Go it",
onClick = {},
)
},
)
}
}
@Suppress("LongMethod")
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemModalBottomSheetWithFooter_Preview2(
@PreviewParameter(TangemBottomSheetPreviewProvider::class) params: TangemBottomSheetType,
) {
TangemThemePreviewRedesign {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
type = params,
title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) },
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
Text(
text = "Long text to show scrollable content and bottom fade." +
"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " +
"ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " +
"consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " +
"dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " +
"habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
"Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " +
"pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " +
"turpis pellentesque rhoncus.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
},
)
}
}
private class TangemBottomSheetPreviewProvider : PreviewParameterProvider<TangemBottomSheetType> {
override val values: Sequence<TangemBottomSheetType>
get() = sequenceOf(
Default,
Modal,
)
}
// endregion

View file

@ -0,0 +1,88 @@
package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InternalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = 0.dp,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
val topSnackbarHostState = LocalTopSnackbarHostState.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
content = {
Box {
val hazeState = rememberHazeState()
Column(Modifier.hazeSourceTangem(hazeState)) {
content()
}
CompositionLocalProvider(LocalHazeState provides hazeState) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(top = TangemTheme.dimens2.x6),
hostState = topSnackbarHostState,
)
}
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke()
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {
launch { hide() }.invokeOnCompletion { onCollapsed() }
}
}

View file

@ -8,11 +8,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -48,17 +45,9 @@ fun ModalBottomSheetWithBackHandling(
), ),
content = { content = {
content() content()
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) { BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke() onBack?.invoke()
} }
}, },
) )
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {
launch { hide() }.invokeOnCompletion { onCollapsed() }
}
} }

View file

@ -1,156 +1,293 @@
package com.tangem.core.ui.components.bottomsheets.message package com.tangem.core.ui.components.bottomsheets.message
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM.Button.IconOrder
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable @Composable
fun MessageBottomSheet(config: TangemBottomSheetConfig) { fun MessageBottomSheet(state: MessageBottomSheetUM, onDismissRequest: () -> Unit) {
TangemBottomSheet(config) { notification: MessageBottomSheetUM -> val stateWithOnDismiss = remember(state) {
Content(model = notification) state.copy(
onDismissRequest = {
state.onDismissRequest.invoke()
onDismissRequest()
},
)
}
val config = TangemBottomSheetConfig(
isShown = true,
content = stateWithOnDismiss,
onDismissRequest = stateWithOnDismiss.onDismissRequest,
)
TangemModalBottomSheet(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = stateWithOnDismiss.onDismissRequest,
)
},
content = { content: MessageBottomSheetUM -> MessageBottomSheetContent(content) },
)
}
@Composable
fun MessageBottomSheetContent(state: MessageBottomSheetUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.elements.fastForEach { element ->
when (element) {
is MessageBottomSheetUM.InfoBlock -> {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
state = element,
)
}
else -> Unit
}
}
ButtonsContainer(
modifier = Modifier.fillMaxWidth(),
closeScope = state.closeScope,
buttons = state.elements.filterIsInstance<MessageBottomSheetUM.Button>().toPersistentList(),
)
} }
} }
@Composable @Composable
internal fun Content(model: MessageBottomSheetUM, modifier: Modifier = Modifier) { private fun ContentContainer(state: MessageBottomSheetUM.InfoBlock, modifier: Modifier = Modifier) {
Column( Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
modifier = modifier.padding(horizontal = 16.dp), BottomSheetIconContainer(state.icon, state.iconImage)
verticalArrangement = Arrangement.spacedBy(40.dp), state.title?.let { title ->
horizontalAlignment = Alignment.CenterHorizontally, Text(
) { modifier = Modifier
Box(modifier = Modifier) .fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
if (model.iconResId != null) { text = title.resolveReference(),
Icon( style = TangemTheme.typography.h3,
modifier = Modifier.size(48.dp), color = TangemTheme.colors.text.primary1,
painter = painterResource(model.iconResId), textAlign = TextAlign.Center,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
) )
} }
state.body?.let { body ->
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (!model.title.isNullOrEmpty()) {
Text(
text = model.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
Text( Text(
text = model.message.resolveReference(), modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
text = body.resolveAnnotatedReference(),
style = TangemTheme.typography.body2, style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }
state.chip?.let { chip ->
Column( BottomSheetChip(
modifier = Modifier modifier = Modifier.padding(top = TangemTheme.dimens.spacing16),
.padding(bottom = 16.dp) chip = chip,
.fillMaxWidth(), )
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (model.primaryAction != null) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = model.primaryAction.text.resolveReference(),
enabled = model.primaryAction.isEnabled,
onClick = model.primaryAction.onClick,
)
}
if (model.secondaryAction != null) {
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = model.secondaryAction.text.resolveReference(),
enabled = model.secondaryAction.isEnabled,
onClick = model.secondaryAction.onClick,
)
}
} }
} }
} }
// region Preview
@Composable @Composable
@Preview(showBackground = true, widthDp = 360) private fun BottomSheetIconContainer(
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) icon: MessageBottomSheetUM.Icon?,
private fun Preview_NotificationBottomSheet( iconImage: MessageBottomSheetUM.IconImage?,
@PreviewParameter(MessageBottomSheetUMPreviewProvider::class) params: MessageBottomSheetUM, modifier: Modifier = Modifier,
) { ) {
TangemThemePreview { if (icon != null) {
MessageBottomSheet( BottomSheetIcon(icon, modifier)
config = TangemBottomSheetConfig( } else if (iconImage != null) {
content = params, Image(
isShown = true, modifier = modifier
onDismissRequest = {}, .size(TangemTheme.dimens.size56)
), .clip(CircleShape),
painter = painterResource(id = iconImage.res),
contentDescription = null,
) )
} }
} }
private class MessageBottomSheetUMPreviewProvider : PreviewParameterProvider<MessageBottomSheetUM> { @Composable
val balancesHiddenMessage = MessageBottomSheetUM( private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) {
iconResId = R.drawable.ic_eye_off_outline_24, val tint = when (icon.type) {
title = resourceReference(R.string.balance_hidden_title), MessageBottomSheetUM.Icon.Type.Unspecified -> Color.Unspecified
message = resourceReference(R.string.balance_hidden_description), MessageBottomSheetUM.Icon.Type.Accent -> TangemTheme.colors.icon.accent
primaryAction = MessageBottomSheetUM.ActionUM( MessageBottomSheetUM.Icon.Type.Informative -> TangemTheme.colors.icon.informative
text = resourceReference(R.string.balance_hidden_got_it_button), MessageBottomSheetUM.Icon.Type.Attention -> TangemTheme.colors.icon.attention
onClick = {}, MessageBottomSheetUM.Icon.Type.Warning -> TangemTheme.colors.icon.warning
), }
secondaryAction = MessageBottomSheetUM.ActionUM(
text = resourceReference(R.string.balance_hidden_do_not_show_button),
onClick = {},
),
)
override val values: Sequence<MessageBottomSheetUM> val backgroundColor = when (icon.backgroundType) {
get() = sequenceOf( MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> TangemTheme.colors.icon.informative
balancesHiddenMessage, MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint
balancesHiddenMessage.copy(title = null), MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors.icon.accent
balancesHiddenMessage.copy(iconResId = null), MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors.icon.informative
balancesHiddenMessage.copy(primaryAction = null), MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors.icon.attention
balancesHiddenMessage.copy(secondaryAction = null), MessageBottomSheetUM.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
balancesHiddenMessage.copy( }
primaryAction = null,
secondaryAction = null, HighlightedIcon(
), modifier = modifier,
balancesHiddenMessage.copy( icon = icon.res,
title = null, iconTint = tint,
iconResId = null, backgroundColor = backgroundColor,
primaryAction = null, )
secondaryAction = null,
),
)
} }
// endregion Preview
@Composable
private fun BottomSheetChip(chip: MessageBottomSheetUM.Chip, modifier: Modifier = Modifier) {
val color = when (chip.type) {
MessageBottomSheetUM.Chip.Type.Unspecified -> TangemTheme.colors.text.primary1
MessageBottomSheetUM.Chip.Type.Warning -> TangemTheme.colors.text.warning
}
Text(
modifier = modifier
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = color.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = chip.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = color,
)
}
@Suppress("LongMethod")
@Composable
private fun ButtonsContainer(
buttons: ImmutableList<MessageBottomSheetUM.Button>,
closeScope: MessageBottomSheetUM.CloseScope,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
buttons.fastForEach { button ->
val icon = button.icon?.let { iconResId ->
when (button.iconOrder) {
IconOrder.Start -> TangemButtonIconPosition.Start(iconResId)
IconOrder.End -> TangemButtonIconPosition.End(iconResId)
}
} ?: TangemButtonIconPosition.None
TangemButton(
modifier = Modifier.fillMaxWidth(),
text = button.text?.resolveReference().orEmpty(),
icon = icon,
onClick = { button.onClick?.invoke(closeScope) },
colors = if (button.isPrimary) {
TangemButtonsDefaults.primaryButtonColors
} else {
TangemButtonsDefaults.secondaryButtonColors
},
enabled = true,
showProgress = false,
textStyle = TangemTheme.typography.subtitle1,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUM.Icon.Type.Attention
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.SameAsTint
}
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview2() {
TangemThemePreview {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
iconImage = MessageBottomSheetUM.IconImage(R.drawable.img_visa_notification)
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}

View file

@ -1,20 +1,143 @@
package com.tangem.core.ui.components.bottomsheets.message package com.tangem.core.ui.components.bottomsheets.message
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Immutable
data class MessageBottomSheetUM( data class MessageBottomSheetUM(
@DrawableRes val iconResId: Int?, var elements: ImmutableList<Element> = persistentListOf(),
val title: TextReference?, var onDismissRequest: () -> Unit = {},
val message: TextReference,
val primaryAction: ActionUM?,
val secondaryAction: ActionUM?,
) : TangemBottomSheetConfigContent { ) : TangemBottomSheetConfigContent {
data class ActionUM( @Immutable
val text: TextReference, inner class CloseScope {
val isEnabled: Boolean = true, fun closeBs() {
val onClick: () -> Unit, onDismissRequest()
) }
} }
val closeScope = CloseScope()
@Immutable
sealed interface Element
@Immutable
data class Icon(
@DrawableRes internal var res: Int,
var type: Type = Type.Unspecified,
var backgroundType: BackgroundType = BackgroundType.Unspecified,
) : Element {
enum class Type {
Unspecified, Accent, Informative, Attention, Warning,
}
enum class BackgroundType {
Unspecified, SameAsTint, Accent, Informative, Attention, Warning,
}
}
@Immutable
data class IconImage(@DrawableRes internal var res: Int) : Element
@Immutable
data class Chip(
internal var text: TextReference,
var type: Type = Type.Unspecified,
) : Element {
enum class Type {
Unspecified, Warning
}
}
@Immutable
data class InfoBlock(
internal var icon: Icon? = null,
internal var iconImage: IconImage? = null,
internal var chip: Chip? = null,
var title: TextReference? = null,
var body: TextReference? = null,
) : Element
@Immutable
data class Button(
internal var isPrimary: Boolean = false,
var text: TextReference? = null,
@DrawableRes internal var iconInternal: Int? = null,
internal var iconOrder: IconOrder = IconOrder.Start,
var onClick: (CloseScope.() -> Unit)? = null,
) : Element {
var icon: Int? = iconInternal
set(value) {
iconOrder = if (text == null) {
IconOrder.Start
} else {
IconOrder.End
}
field = value
}
enum class IconOrder {
Start, End
}
}
}
@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class MessageBottomSheetDsl
// region: DSL
fun messageBottomSheetUM(init: @MessageBottomSheetDsl MessageBottomSheetUM.() -> Unit) =
MessageBottomSheetUM().apply(init)
fun MessageBottomSheetUM.onDismiss(block: () -> Unit) = apply { onDismissRequest = block }
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.infoBlock(init: @MessageBottomSheetDsl MessageBottomSheetUM.InfoBlock.() -> Unit) = apply {
val element = MessageBottomSheetUM.InfoBlock().apply(init)
elements = (elements + element).toPersistentList()
}
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.InfoBlock.icon(@DrawableRes res: Int, init: MessageBottomSheetUM.Icon.() -> Unit = {}) =
apply {
icon = MessageBottomSheetUM.Icon(res).apply(init)
}
fun MessageBottomSheetUM.InfoBlock.iconImage(@DrawableRes res: Int) = apply {
iconImage = MessageBottomSheetUM.IconImage(res)
}
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUM.Chip.() -> Unit = {}) = apply {
chip = MessageBottomSheetUM.Chip(text).apply(init)
}
@Suppress("NestedScopeFunctions")
internal fun MessageBottomSheetUM.button(init: @MessageBottomSheetDsl MessageBottomSheetUM.Button.() -> Unit) = apply {
val element = MessageBottomSheetUM.Button().apply(init)
elements = (elements + element).toPersistentList()
}
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.primaryButton(init: @MessageBottomSheetDsl MessageBottomSheetUM.Button.() -> Unit) = apply {
button { isPrimary = true; apply(init) }
}
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.secondaryButton(init: @MessageBottomSheetDsl MessageBottomSheetUM.Button.() -> Unit) = apply {
button { isPrimary = false; apply(init) }
}
fun MessageBottomSheetUM.Button.onClick(block: MessageBottomSheetUM.CloseScope.() -> Unit) = apply {
onClick = block
}
// endregion

View file

@ -1,142 +0,0 @@
package com.tangem.core.ui.components.bottomsheets.message
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Immutable
data class MessageBottomSheetUMV2(
var elements: ImmutableList<Element> = persistentListOf(),
var onDismissRequest: () -> Unit = {},
) : TangemBottomSheetConfigContent {
@Immutable
inner class CloseScope {
fun closeBs() {
onDismissRequest()
}
}
val closeScope = CloseScope()
@Immutable
sealed interface Element
@Immutable
data class Icon(
@DrawableRes internal var res: Int,
var type: Type = Type.Unspecified,
var backgroundType: BackgroundType = BackgroundType.Unspecified,
) : Element {
enum class Type {
Unspecified, Accent, Informative, Attention, Warning,
}
enum class BackgroundType {
Unspecified, SameAsTint, Accent, Informative, Attention, Warning,
}
}
@Immutable
data class IconImage(@DrawableRes internal var res: Int) : Element
@Immutable
data class Chip(
internal var text: TextReference,
var type: Type = Type.Unspecified,
) : Element {
enum class Type {
Unspecified, Warning
}
}
@Immutable
data class InfoBlock(
internal var icon: Icon? = null,
internal var iconImage: IconImage? = null,
internal var chip: Chip? = null,
var title: TextReference? = null,
var body: TextReference? = null,
) : Element
@Immutable
data class Button(
internal var isPrimary: Boolean = false,
var text: TextReference? = null,
@DrawableRes internal var iconInternal: Int? = null,
internal var iconOrder: IconOrder = IconOrder.Start,
var onClick: (CloseScope.() -> Unit)? = null,
) : Element {
var icon: Int? = iconInternal
set(value) {
iconOrder = if (text == null) {
IconOrder.Start
} else {
IconOrder.End
}
field = value
}
enum class IconOrder {
Start, End
}
}
}
@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class MessageBottomSheetV2Dsl
// region: DSL
fun messageBottomSheetUM(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.() -> Unit) =
MessageBottomSheetUMV2().apply(init)
fun MessageBottomSheetUMV2.onDismiss(block: () -> Unit) = MessageBottomSheetUMV2().apply { onDismissRequest = block }
fun MessageBottomSheetUMV2.infoBlock(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.InfoBlock.() -> Unit) =
apply {
val element = MessageBottomSheetUMV2.InfoBlock().apply(init)
elements = (elements + element).toPersistentList()
}
fun MessageBottomSheetUMV2.InfoBlock.icon(@DrawableRes res: Int, init: MessageBottomSheetUMV2.Icon.() -> Unit = {}) =
apply {
icon = MessageBottomSheetUMV2.Icon(res).apply(init)
}
fun MessageBottomSheetUMV2.InfoBlock.iconImage(@DrawableRes res: Int) = apply {
iconImage = MessageBottomSheetUMV2.IconImage(res)
}
fun MessageBottomSheetUMV2.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUMV2.Chip.() -> Unit = {}) =
apply {
chip = MessageBottomSheetUMV2.Chip(text).apply(init)
}
internal fun MessageBottomSheetUMV2.button(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
val element = MessageBottomSheetUMV2.Button().apply(init)
elements = (elements + element).toPersistentList()
}
fun MessageBottomSheetUMV2.primaryButton(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
button { isPrimary = true; apply(init) }
}
fun MessageBottomSheetUMV2.secondaryButton(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
button { isPrimary = false; apply(init) }
}
fun MessageBottomSheetUMV2.Button.onClick(block: MessageBottomSheetUMV2.CloseScope.() -> Unit) = apply {
onClick = block
}
// endregion

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