Updated on 2026-08-14
This commit is contained in:
commit
20279dcc7a
602 changed files with 18887 additions and 6569 deletions
|
|
@ -366,6 +366,7 @@ dependencies {
|
|||
implementation(deps.googlePlay.advertising)
|
||||
coreLibraryDesugaring(deps.desugar)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kermit)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.zxing.qrCore)
|
||||
implementation(deps.coil)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.common
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.tap.ApplicationEntryPoint
|
||||
import com.tangem.tap.TangemApplication
|
||||
import dagger.hilt.android.testing.OnComponentReadyRunner
|
||||
|
|
@ -10,7 +11,7 @@ import org.junit.runners.model.Statement
|
|||
import timber.log.Timber
|
||||
|
||||
class ApplicationInjectionExecutionRule(
|
||||
private val toggleStates: Map<String, Boolean>
|
||||
private val toggleStates: Map<String, Boolean>,
|
||||
) : TestRule {
|
||||
|
||||
private val tangemApplication: TangemApplication
|
||||
|
|
@ -25,7 +26,7 @@ class ApplicationInjectionExecutionRule(
|
|||
overrideFeatureToggles()
|
||||
|
||||
OnComponentReadyRunner.addListener(
|
||||
tangemApplication, ApplicationEntryPoint::class.java
|
||||
tangemApplication, ApplicationEntryPoint::class.java,
|
||||
) { _: ApplicationEntryPoint ->
|
||||
tangemApplication.preInit()
|
||||
tangemApplication.init()
|
||||
|
|
@ -43,25 +44,15 @@ class ApplicationInjectionExecutionRule(
|
|||
@Suppress("UNCHECKED_CAST")
|
||||
private fun saveOriginalFeatureToggles() {
|
||||
try {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
originalFeatureTogglesValues = valuesField.get(null) as Map<String, String>
|
||||
originalFeatureTogglesValues = FeatureToggles.values as Map<String, String>
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to save original toggles values: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun overrideFeatureToggles() {
|
||||
try {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
|
||||
val originalValues = originalFeatureTogglesValues ?:
|
||||
(valuesField.get(null) as Map<String, String>)
|
||||
|
||||
val originalValues = originalFeatureTogglesValues ?: FeatureToggles.values
|
||||
val newValues = originalValues.toMutableMap()
|
||||
|
||||
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")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e("FeatureToggles.values didn't change with error: ${e.message}")
|
||||
}
|
||||
|
|
@ -84,10 +77,10 @@ class ApplicationInjectionExecutionRule(
|
|||
private fun restoreOriginalFeatureToggles() {
|
||||
try {
|
||||
if (originalFeatureTogglesValues != null) {
|
||||
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
|
||||
val valuesField = featureTogglesClass.getDeclaredField("values")
|
||||
val companionClass = FeatureToggles.Companion::class.java
|
||||
val valuesField = companionClass.getDeclaredField("values")
|
||||
valuesField.isAccessible = true
|
||||
valuesField.set(null, originalFeatureTogglesValues)
|
||||
valuesField.set(FeatureToggles.Companion, originalFeatureTogglesValues)
|
||||
Timber.i("FeatureToggles.values restored")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.junit.Rule
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class BaseTestCase : TestCase(
|
||||
|
|
@ -68,6 +70,12 @@ abstract class BaseTestCase : TestCase(
|
|||
*/
|
||||
val composeTestRule = createEmptyComposeRule()
|
||||
|
||||
private val semanticTreePrinterRule = object : TestWatcher() {
|
||||
override fun failed(e: Throwable?, description: Description?) {
|
||||
runCatching { printAllRoots() }
|
||||
}
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleChain: TestRule = RuleChain
|
||||
|
|
@ -76,6 +84,7 @@ abstract class BaseTestCase : TestCase(
|
|||
.around(permissionRule)
|
||||
.around(apiEnvironmentRule)
|
||||
.around(composeTestRule)
|
||||
.around(semanticTreePrinterRule)
|
||||
|
||||
/**
|
||||
* Initialization order is important:
|
||||
|
|
@ -140,6 +149,16 @@ abstract class BaseTestCase : TestCase(
|
|||
.printToLog(tag, maxDepth)
|
||||
}
|
||||
|
||||
fun printAllRoots(
|
||||
tag: String = "ComposeTree",
|
||||
) {
|
||||
val roots = composeTestRule.onAllNodes(isRoot())
|
||||
val count = roots.fetchSemanticsNodes().size
|
||||
repeat(count) { index ->
|
||||
roots[index].printToLog("$tag[$index]")
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForIdle() = composeTestRule.waitForIdle()
|
||||
|
||||
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.components.buttons.actions.HasBadgeKey
|
||||
import com.tangem.core.ui.components.buttons.actions.IsDimmedKey
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
fun assertElementDoesNotExist(
|
||||
elementProvider: () -> KNode,
|
||||
|
|
@ -42,4 +46,51 @@ fun Any.assertHasBadge(expectedValue: Boolean = true) {
|
|||
is KNode, is LazyListItemNode -> this.assert(matcher)
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${this::class}")
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertSortedByVolumeDescending() {
|
||||
val volumes = this.mapNotNull { parseVolume(it) }
|
||||
assertFalse("Trading volumes list should not be empty", volumes.isEmpty())
|
||||
assertTrue(
|
||||
"Exchanges list should be sorted by volume in descending order",
|
||||
volumes == volumes.sortedDescending(),
|
||||
)
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertExchangeTypesAreCexOrDex() {
|
||||
assertFalse("Exchange types list should not be empty", isEmpty())
|
||||
forEach { node ->
|
||||
val text = extractText(node)
|
||||
assertTrue(
|
||||
"Exchange type should be 'CEX' or 'DEX', but found: $text",
|
||||
text == "CEX" || text == "DEX",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SemanticsNode>.assertTrustScoresValid() {
|
||||
val validScores = setOf("Risky", "Caution", "Trusted")
|
||||
assertFalse("Trust scores list should not be empty", isEmpty())
|
||||
forEach { node ->
|
||||
val text = extractText(node)
|
||||
assertTrue(
|
||||
"Trust score should be one of $validScores, but found: $text",
|
||||
text in validScores,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text value from a semantic node's config.
|
||||
*/
|
||||
private fun extractText(node: SemanticsNode): String? {
|
||||
if (SemanticsProperties.Text in node.config) {
|
||||
return node.config[SemanticsProperties.Text].firstOrNull()?.text
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseVolume(node: SemanticsNode): Double? {
|
||||
val text = extractText(node) ?: return null
|
||||
return text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull()
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.test.ComposeTimeoutException
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
||||
fun KNode.clickWithAssertion() {
|
||||
|
|
@ -34,3 +36,34 @@ fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
|
|||
assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
fun KNode.clickAndWaitFor(
|
||||
rule: ComposeTestRule,
|
||||
timeoutMs: Long = 5_000,
|
||||
maxRetries: Int = 3,
|
||||
expectedCondition: () -> Unit,
|
||||
) {
|
||||
for (attempt in 1..maxRetries) {
|
||||
performClick()
|
||||
rule.waitForIdle()
|
||||
|
||||
try {
|
||||
rule.waitUntil(timeoutMs) {
|
||||
runCatching { expectedCondition() }.isSuccess
|
||||
}
|
||||
return
|
||||
} catch (_: ComposeTimeoutException) {
|
||||
}
|
||||
}
|
||||
|
||||
throw AssertionError("Condition not met after $maxRetries click attempts")
|
||||
}
|
||||
|
||||
fun KNode.performTextInputInChunks(
|
||||
text: String,
|
||||
chunkSize: Int = 2
|
||||
) {
|
||||
text.chunked(chunkSize).forEach { chunk ->
|
||||
performTextInput(chunk)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -46,7 +47,23 @@ fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
|
|||
}
|
||||
|
||||
fun BaseTestCase.openTheAppFromRecents() {
|
||||
device.uiDevice.waitForIdle()
|
||||
|
||||
device.uiDevice.pressRecentApps()
|
||||
device.uiDevice.waitForIdle()
|
||||
|
||||
val recentsOpened = device.uiDevice.wait(
|
||||
Until.hasObject(By.res("com.android.launcher3:id/snapshot")),
|
||||
3_000
|
||||
)
|
||||
|
||||
if (!recentsOpened) {
|
||||
device.uiDevice.pressRecentApps()
|
||||
device.uiDevice.wait(
|
||||
Until.hasObject(By.res("com.android.launcher3:id/snapshot")),
|
||||
3_000
|
||||
)
|
||||
}
|
||||
|
||||
val centerX = device.uiDevice.displayWidth / 2
|
||||
val centerY = device.uiDevice.displayHeight / 3
|
||||
|
|
@ -54,6 +71,14 @@ fun BaseTestCase.openTheAppFromRecents() {
|
|||
device.uiDevice.click(centerX, centerY)
|
||||
}
|
||||
|
||||
fun BaseTestCase.collapseAppByHomeButton() {
|
||||
device.uiDevice.pressHome()
|
||||
device.uiDevice.wait(
|
||||
Until.hasObject(By.pkg(device.uiDevice.launcherPackageName)),
|
||||
3_000
|
||||
)
|
||||
}
|
||||
|
||||
fun BaseTestCase.disableWiFi() {
|
||||
device.uiDevice.executeShellCommand("svc wifi disable")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.SwipeDirection
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.swipeMarketsBlock
|
||||
import com.tangem.common.extensions.swipeVertical
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onMarketsExchangesScreen
|
||||
import com.tangem.screens.onMarketsScreen
|
||||
import com.tangem.screens.onMarketsTokenDetailsScreen
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
|
@ -23,4 +27,55 @@ fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName:
|
|||
waitForIdle()
|
||||
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.assertMarketsExchangesScreen() {
|
||||
step("Assert 'Title' is displayed") {
|
||||
onMarketsExchangesScreen { exchangesTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Exchange name' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeName.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Logo' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeLogo.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Exchange type' is displayed") {
|
||||
onMarketsExchangesScreen { exchangeType.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Trust score' is displayed") {
|
||||
onMarketsExchangesScreen { trustScore.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openMarketsScreen() {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Markets' screen") {
|
||||
swipeMarketsBlock(SwipeDirection.UP)
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
||||
openMarketsScreen()
|
||||
if (shouldClickSeeAllButton)
|
||||
step("Click on 'See all' button") {
|
||||
onMarketsScreen { seeAllButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on '$tokenName' token") {
|
||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Scroll down") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click on 'Listed on exchanges' block") {
|
||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.common_add_token))
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasParent
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
class MarketsExchangesPageObject(private val provider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<MarketsExchangesPageObject>(semanticsProvider = provider) {
|
||||
|
||||
fun allTradingVolumeNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT_TEXT))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasParent(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
fun allTrustScoreNodes(): List<SemanticsNode> =
|
||||
provider
|
||||
.onAllNodes(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)))
|
||||
.fetchSemanticsNodes()
|
||||
|
||||
val exchangesTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val exchangeName: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
}
|
||||
|
||||
val exchangeLogo: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_ICON)
|
||||
}
|
||||
|
||||
val tradingVolume: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
|
||||
}
|
||||
|
||||
val exchangeType: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_PRICE)
|
||||
}
|
||||
|
||||
val trustScore: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)
|
||||
}
|
||||
|
||||
val tryAgainButton: KNode = child {
|
||||
hasText(getResourceString(R.string.alert_button_try_again))
|
||||
}
|
||||
|
||||
val errorMessage: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_loading_error_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onMarketsExchangesScreen(function: MarketsExchangesPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -35,6 +35,25 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val listedOnExchangesCount: KNode = child {
|
||||
hasTestTag(MarketsTestTags.LISTED_ON_EXCHANGES_COUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val listedOnBlockContainer: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_token_details_listed_on), substring = true)
|
||||
}
|
||||
|
||||
val listedOnEmptyText: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_token_details_empty_exchanges))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val seeAllButton: KNode = child {
|
||||
hasText(getResourceString(com.tangem.core.ui.R.string.common_see_all))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenWithTitle(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addressesShimmer: KNode = child {
|
||||
hasTestTag(SendAddressScreenTestTags.ADDRESSES_SHIMMER)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val topAppBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(CoreUiR.string.common_address))
|
||||
|
|
@ -62,7 +67,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
}
|
||||
|
||||
val clearTextFieldButton: KNode = child {
|
||||
hasContentDescription(getResourceString(CoreUiR.string.common_close))
|
||||
hasTestTag(SendAddressScreenTestTags.CROSS_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.AppBarWithSearchTestTags
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
|
@ -46,6 +47,13 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
}
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun marketsTokenWithTitle(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
hasText(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ package com.tangem.screens
|
|||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.SearchBarTestTags
|
||||
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.test.*
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
|
@ -48,6 +45,11 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val searchBarBlock: KNode = child {
|
||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val searchBarIcon: KNode = child {
|
||||
hasTestTag(SearchBarTestTags.ICON)
|
||||
useUnmergedTree = true
|
||||
|
|
@ -58,12 +60,28 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val tryAgainButton: KNode = child {
|
||||
hasText(getResourceString(R.string.alert_button_try_again))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val unableToLoadData: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_loading_error_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenWithName(tokenName: String): KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
hasAnyChild(withText(tokenName))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun marketsTokenWithName(title: String): KNode {
|
||||
return child {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
hasText(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
|
||||
}
|
||||
|
||||
val swapTokensOnscreenButton: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON)
|
||||
val replaceTokensButton: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.REPLACE_TOKENS_BUTTON)
|
||||
}
|
||||
|
||||
val receiveAmount: KNode = child {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class TopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
) {
|
||||
val moreButton: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.clickAndWaitFor
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.tap.store
|
|||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -45,6 +47,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
onTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click 'Contact support' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { contactSupportButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
|
|
@ -53,6 +56,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("893")
|
||||
@DisplayName("Send feedback: failed transaction")
|
||||
@Test
|
||||
|
|
@ -94,17 +98,29 @@ class FeedbackTest : BaseTestCase() {
|
|||
step("Enter address") {
|
||||
onSendAddressScreen { addressTextField.performTextInput(recipientAddress) }
|
||||
}
|
||||
step("Click 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
step("Assert text field contains text: $recipientAddress") {
|
||||
onSendAddressScreen { addressTextField.assertTextEquals(recipientAddress) }
|
||||
}
|
||||
step("Assert sеnding text is displayed") {
|
||||
onSendConfirmScreen { sendingText.assertIsDisplayed() }
|
||||
step("Click 'Next' button") {
|
||||
onSendAddressScreen {
|
||||
nextButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendConfirmScreen { sendingText.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Click 'Send' button") {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen {
|
||||
sendButton.assertIsEnabled()
|
||||
sendButton.performClick()
|
||||
sendButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onFailedTransactionDialog { dialogContainer.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Check 'Failed transaction' dialog") {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.tap.domain.sdk.mocks.content.*
|
|||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -85,6 +86,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("870")
|
||||
@DisplayName("Scan: Card with Ed25519 curve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
|
|||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -460,6 +461,7 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4396")
|
||||
@DisplayName("Action buttons (main screen): click on buttons without data")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
|
|||
val scenarioName = "user_tokens_api"
|
||||
val scenarioState = "CustomTokenAdded"
|
||||
val tokenTitle = "Myria"
|
||||
val balance = "$3,299.18"
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
|
|
@ -134,8 +135,8 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
step("Assert dash sign is displayed in total balance") {
|
||||
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
|
||||
step("Assert correct balance is displayed in total balance") {
|
||||
onMainScreen { totalBalanceText.assertTextContains(balance) }
|
||||
}
|
||||
step("Assert $tokenTitle is unreachable") {
|
||||
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 4).assertIsUnreachable() }
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
|||
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
||||
}
|
||||
step("Press 'Home' to collapse the app") {
|
||||
device.uiDevice.pressHome()
|
||||
collapseAppByHomeButton()
|
||||
}
|
||||
step("Open the app from recent apps") {
|
||||
openTheAppFromRecents()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
package com.tangem.tests.markets
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.annotations.ApiEnv
|
||||
import com.tangem.common.annotations.ApiEnvConfig
|
||||
import com.tangem.common.extensions.*
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.scenarios.assertMarketsExchangesScreen
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openMarketsExchangesScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMarketsExchangesScreen
|
||||
import com.tangem.screens.onMarketsScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class MarketsExchangesTest : BaseTestCase() {
|
||||
|
||||
@Test
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@AllureId("58")
|
||||
@DisplayName("Markets: verify exchanges list screen")
|
||||
fun marketsExchangesListTest() {
|
||||
val tokenName = "Solana"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName = tokenName, shouldClickSeeAllButton = true)
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("56")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchanges block is displayed in token details")
|
||||
fun marketsExchangesBlockDisplayedTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Markets' screen") {
|
||||
swipeMarketsBlock(SwipeDirection.UP)
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on '$tokenName' token") {
|
||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Scroll down") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has title") {
|
||||
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has exchanges count") {
|
||||
onMarketsScreen { listedOnExchangesCount.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block has arrow button") {
|
||||
onMarketsScreen { listedOnBlockContainer.assertHasClickAction() }
|
||||
}
|
||||
step("Tap on 'Listed on exchanges' block and navigate to exchanges list") {
|
||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("60")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchanges list is sorted by volume descending")
|
||||
fun marketsExchangesListSortedByVolumeTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert exchanges are sorted by trading volume in descending order") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allTradingVolumeNodes().assertSortedByVolumeDescending() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("61")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchange types are CEX or DEX")
|
||||
fun marketsExchangesTypeTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert exchange types list is not empty and all types are 'CEX' or 'DEX'") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allExchangeTypeNodes().assertExchangeTypesAreCexOrDex() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("62")
|
||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||
@DisplayName("Markets: verify exchange trust scores are valid")
|
||||
fun marketsExchangesTrustScoreTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
setupHooks().run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert trust scores list is not empty and all scores are valid") {
|
||||
flakySafely {
|
||||
onMarketsExchangesScreen { allTrustScoreNodes().assertTrustScoresValid() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("57")
|
||||
@DisplayName("Markets: verify empty exchanges state")
|
||||
fun marketsExchangesEmptyTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val scenarioName = "coins_bitcoin"
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "EmptyExchanges")
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
},
|
||||
).run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert 'Listed on exchanges' block shows no exchanges") {
|
||||
onMarketsScreen { listedOnEmptyText.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@AllureId("59")
|
||||
@DisplayName("Markets: verify exchanges error state and retry")
|
||||
fun marketsExchangesErrorStateTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val scenarioName = "bitcoin_exchange"
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "Unreachable")
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
},
|
||||
).run {
|
||||
step("Open 'Markets Exhanges Screen with token: $tokenName'") {
|
||||
openMarketsExchangesScreen(tokenName)
|
||||
}
|
||||
step("Assert 'Unable to load data' error state is displayed") {
|
||||
step("Assert 'Error message' is displayed") {
|
||||
onMarketsExchangesScreen { errorMessage.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Try again button' is displayed") {
|
||||
onMarketsExchangesScreen { tryAgainButton.assertIsDisplayed() }
|
||||
}
|
||||
|
||||
}
|
||||
step("Set WireMock scenario '$scenarioName' to state 'Started'") {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = "Started")
|
||||
}
|
||||
step("Tap 'Try again' button") {
|
||||
onMarketsExchangesScreen { tryAgainButton.clickWithAssertion() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Exchanges' list screen is displayed after retry") {
|
||||
assertMarketsExchangesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_SHORTENE
|
|||
import com.tangem.common.constants.TestConstants.ENS_NAME
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS_WITH_TAG
|
||||
import com.tangem.common.extensions.clickAndWaitFor
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.clearClipboard
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
|
|
@ -115,6 +117,9 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
clearClipboard()
|
||||
}
|
||||
).run {
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, recipientAddress)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
|
|
@ -124,10 +129,11 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Open 'Send Address' screen") {
|
||||
openSendAddressScreen(tokenName, sendAmount)
|
||||
}
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, recipientAddress)
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
waitForIdle()
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert address text field contains correct recipient address") {
|
||||
|
|
@ -142,8 +148,22 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Set clipboard text") {
|
||||
setClipboardText(context, invalidAddress)
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Cross' button") {
|
||||
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
|
||||
waitForIdle()
|
||||
onSendAddressScreen {
|
||||
clearTextFieldButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendAddressScreen { addressPasteButton.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
|
|
@ -154,20 +174,38 @@ class SendAddressScreenTest : BaseTestCase() {
|
|||
step("Assert invalid address text field title is displayed") {
|
||||
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
|
||||
}
|
||||
step("Assert 'Next' button is disabled") {
|
||||
onSendAddressScreen { nextButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Set clipboard text") {
|
||||
setClipboardText(context, walletAddress)
|
||||
}
|
||||
step("Assert 'Next' button is disabled") {
|
||||
onSendAddressScreen { nextButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Cross' button") {
|
||||
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
|
||||
onSendAddressScreen {
|
||||
clearTextFieldButton.clickAndWaitFor(
|
||||
rule = composeTestRule,
|
||||
expectedCondition = {
|
||||
onSendAddressScreen { addressPasteButton.assertIsDisplayed() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
step("Assert 'Addresses shimmer' is not displayed") {
|
||||
onSendAddressScreen { addressesShimmer.assertDoesNotExist() }
|
||||
}
|
||||
step("Click on 'Paste' button") {
|
||||
waitForIdle()
|
||||
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert address text field contains invalid address") {
|
||||
onSendAddressScreen { addressTextField.assertTextContains(walletAddress) }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendAddressScreen {
|
||||
addressTextField.assertTextContains(walletAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert 'Address is the same as wallet address' error title is displayed") {
|
||||
onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) }
|
||||
|
|
|
|||
|
|
@ -267,22 +267,21 @@ class SendConfirmScreenTest : BaseTestCase() {
|
|||
step("Click on 'Next' button") {
|
||||
onSendScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Turn off internet") {
|
||||
disableWiFi()
|
||||
disableMobileData()
|
||||
}
|
||||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Turn on internet") {
|
||||
enableWiFi()
|
||||
enableMobileData()
|
||||
}
|
||||
step("Assert 'Network fee info unreachable' warning title is displayed") {
|
||||
onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Check your internet connection' warning message is displayed") {
|
||||
onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() }
|
||||
|
|
@ -290,6 +289,10 @@ class SendConfirmScreenTest : BaseTestCase() {
|
|||
step("Assert warning icon is displayed") {
|
||||
onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Turn on internet") {
|
||||
enableWiFi()
|
||||
enableMobileData()
|
||||
}
|
||||
step("Click on 'Refresh' button") {
|
||||
waitForIdle()
|
||||
onSendConfirmScreen {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
|
|||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -32,6 +33,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
|
||||
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4287")
|
||||
@DisplayName("Warnings: check warning, when sending less than reserve")
|
||||
@Test
|
||||
|
|
@ -85,6 +87,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4286")
|
||||
@DisplayName("Warnings: check warning when sending amount equal to reserve")
|
||||
@Test
|
||||
|
|
@ -139,6 +142,7 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4288")
|
||||
@DisplayName("Warnings: check warning when sending greater than reserve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
package com.tangem.tests.swap
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.R
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.SwapEntryPoint
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openSwapScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class SearchAndSwapTest : BaseTestCase() {
|
||||
|
||||
@AllureId("8520")
|
||||
@DisplayName("Search and Swap: add token without derivation")
|
||||
@Test
|
||||
fun addTokenWithoutDerivationTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "Tether"
|
||||
val swapTokenSymbol = "ETH"
|
||||
val receiveTokenSymbol = "USDT"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenName' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8519")
|
||||
@DisplayName("Search and Swap: add token with derivation")
|
||||
@Test
|
||||
fun addTokenWithDerivationTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "TRON"
|
||||
val swapTokenSymbol = "ETH"
|
||||
val receiveTokenSymbol = "TRX"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenSymbol' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenSymbol) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8523")
|
||||
@DisplayName("Search and Swap: Markets error")
|
||||
@Test
|
||||
fun marketsErrorTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val scenarioName = "coins_list_api"
|
||||
val scenarioState = "Error"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(scenarioName, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert 'Unable to load data...' error is displayed") {
|
||||
onSwapSelectTokenScreen { unableToLoadData.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Try again' button is displayed") {
|
||||
onSwapSelectTokenScreen { tryAgainButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8522")
|
||||
@DisplayName("Search and Swap: check 'Unsupported token pair' warning")
|
||||
@Test
|
||||
fun unsupportedTokenPairTest() {
|
||||
val swapTokenName = "Ethereum"
|
||||
val receiveTokenName = "Pepe"
|
||||
val warningTitle = getResourceString(R.string.warning_express_unsupported_pair_title)
|
||||
val warningMessage = getResourceString(R.string.warning_express_unsupported_pair_description)
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||
}
|
||||
step("Click on token with name '$swapTokenName'") {
|
||||
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
|
||||
}
|
||||
step("Type '$receiveTokenName' in input text field") {
|
||||
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
|
||||
}
|
||||
step("Click on token with name '$receiveTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert warning title '$warningTitle' is displayed") {
|
||||
onSwapTokenScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning message '$warningMessage' is displayed") {
|
||||
onSwapTokenScreen { warningMessage(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning icon is displayed'") {
|
||||
onSwapTokenScreen { warningIcon(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8521")
|
||||
@DisplayName("Swap: search token on Swap token screen")
|
||||
@Test
|
||||
fun networkFeeTest() {
|
||||
val tokenTitle = "Ethereum"
|
||||
val swapTokenSymbol = "TRX"
|
||||
val receiveTokenSymbol = "ETH"
|
||||
val swapTokenName = "TRON"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
||||
}
|
||||
step("Click on 'Replace tokens' button") {
|
||||
onSwapTokenScreen { replaceTokensButton.performClick() }
|
||||
}
|
||||
step("Click on 'Select token' icon") {
|
||||
onSwapTokenScreen { selectTokenIcon.performClick() }
|
||||
}
|
||||
step("Click on 'Search' icon") {
|
||||
onSwapChooseTokenScreen { searchIcon.performClick() }
|
||||
}
|
||||
step("Click on 'Search' text field") {
|
||||
onSwapChooseTokenScreen { searchTextField.performClick() }
|
||||
}
|
||||
step("Type '$swapTokenSymbol' in 'Search' text field") {
|
||||
onSwapChooseTokenScreen { searchTextField.performTextInputInChunks(swapTokenSymbol) }
|
||||
}
|
||||
step("Click on token with name: '$swapTokenName'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onSwapChooseTokenScreen { marketsTokenWithTitle(swapTokenName).performClick() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.swap
|
|||
import androidx.compose.ui.test.longClick
|
||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.extensions.assertHasBadge
|
||||
import com.tangem.common.extensions.restartApp
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
|
|
@ -208,7 +209,10 @@ class SwapStoriesTest : BaseTestCase() {
|
|||
}
|
||||
step("Assert 'Swap' button has badge") {
|
||||
waitForIdle()
|
||||
onMainScreen { swapButton.assertHasBadge() }
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
composeTestRule.mainClock.advanceTimeBy(500)
|
||||
onMainScreen { swapButton.assertHasBadge() }
|
||||
}
|
||||
}
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +307,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +397,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||
onSwapTokenScreen {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
swapTokensOnscreenButton.assertIsDisplayed()
|
||||
replaceTokensButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -442,7 +442,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Swap tokens on screen' button") {
|
||||
onSwapTokenScreen { swapTokensOnscreenButton.performClick() }
|
||||
onSwapTokenScreen { replaceTokensButton.performClick() }
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") {
|
||||
|
|
@ -557,9 +557,6 @@ class SwapTokenScreenTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiEnv(
|
||||
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
|
||||
)
|
||||
@AllureId("8536")
|
||||
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
|||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -139,8 +138,6 @@ interface ApplicationEntryPoint {
|
|||
|
||||
fun getApiConfigsManager(): ApiConfigsManager
|
||||
|
||||
fun getUserTokensResponseStore(): UserTokensResponseStore
|
||||
|
||||
fun getUserWalletsListRepository(): UserWalletsListRepository
|
||||
|
||||
fun getTangemHotSdk(): TangemHotSdk
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
|||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
|
|
@ -219,9 +218,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val apiConfigsManager: ApiConfigsManager
|
||||
get() = entryPoint.getApiConfigsManager()
|
||||
|
||||
private val userTokensResponseStore: UserTokensResponseStore
|
||||
get() = entryPoint.getUserTokensResponseStore()
|
||||
|
||||
private val userWalletsListRepository
|
||||
get() = entryPoint.getUserWalletsListRepository()
|
||||
|
||||
|
|
@ -385,7 +381,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
settingsManager = settingsManager,
|
||||
uiMessageSender = uiMessageSender,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
trackingContextProxy = trackingContextProxy,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@ package com.tangem.tap.common.analytics.appsflyer
|
|||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -20,10 +18,9 @@ import kotlin.contracts.contract
|
|||
class AppsFlyerReferralParamsHandler @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob())
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun handle(deepLink: DeepLink) {
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
|
|||
import com.tangem.core.analytics.api.EventLogger
|
||||
import com.tangem.core.analytics.api.UserIdHolder
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener
|
||||
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -26,15 +24,13 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
@ApplicationContext private val context: Context,
|
||||
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
|
||||
tangemAFConversionListener: TangemAFConversionListener,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) : AppsFlyerAnalyticsClient {
|
||||
|
||||
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
|
||||
private val eventConverter = UnderscoreAnalyticsEventConverter()
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
init {
|
||||
with(appsFlyerLib) {
|
||||
setAppId(context.packageName)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import co.touchlab.kermit.BaseLogger
|
||||
import co.touchlab.kermit.LogWriter
|
||||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
import com.orhanobut.logger.Logger as PrettyLogger
|
||||
|
||||
/**
|
||||
* Tangem app logger
|
||||
|
|
@ -21,32 +27,102 @@ class TangemAppLoggerInitializer(
|
|||
/** Initialize */
|
||||
fun initialize() {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
}
|
||||
|
||||
Timber.plant(tree = createTimberTree())
|
||||
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
|
||||
}
|
||||
|
||||
private fun createTimberTree(): Timber.Tree {
|
||||
return object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.log(priority, tag, message, t)
|
||||
}
|
||||
|
||||
if (PERMITTED_PRIORITY.contains(priority)) {
|
||||
appLogsStore.saveLogMessage(
|
||||
tag = tag ?: "TangemAppLogger",
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
finalLogOutput(priority = priority, tag = tag, message = message, t = t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (IS_LOG_ENABLED) {
|
||||
PrettyLogger.log(priority, tag, message, t)
|
||||
}
|
||||
|
||||
if (PERMITTED_PRIORITY.contains(priority)) {
|
||||
appLogsStore.saveLogMessage(
|
||||
tag = tag ?: "TangemAppLogger",
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
private companion object {
|
||||
val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED
|
||||
val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO)
|
||||
}
|
||||
}
|
||||
|
||||
private class KermitLogWriter(
|
||||
private val finalLogOutput: (priority: Int, tag: String?, message: String, t: Throwable?) -> Unit,
|
||||
) : LogWriter() {
|
||||
|
||||
private val fqcnIgnore = setOf(
|
||||
LogWriter::class.java.name,
|
||||
KermitLogWriter::class.java.name,
|
||||
BaseLogger::class.java.name,
|
||||
Logger::class.java.name,
|
||||
)
|
||||
|
||||
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
|
||||
val priority = when (severity) {
|
||||
Severity.Verbose -> PrettyLogger.VERBOSE
|
||||
Severity.Debug -> PrettyLogger.DEBUG
|
||||
Severity.Info -> PrettyLogger.INFO
|
||||
Severity.Warn -> PrettyLogger.WARN
|
||||
Severity.Error -> PrettyLogger.ERROR
|
||||
Severity.Assert -> PrettyLogger.ASSERT
|
||||
}
|
||||
|
||||
val finalTag = if (tag != KERMIT_LOGGER_DEFAULT_TAG) {
|
||||
tag
|
||||
} else {
|
||||
/**
|
||||
* like in [Timber.DebugTree.tag]
|
||||
*/
|
||||
@Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause")
|
||||
Throwable().stackTrace
|
||||
.first { it.className !in fqcnIgnore }
|
||||
.let(::createStackElementTag)
|
||||
}
|
||||
|
||||
finalLogOutput(priority, finalTag, message, throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* copy from [Timber.DebugTree.createStackElementTag]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun createStackElementTag(element: StackTraceElement): String? {
|
||||
var tag = element.className.substringAfterLast('.')
|
||||
val m = ANONYMOUS_CLASS.matcher(tag)
|
||||
if (m.find()) {
|
||||
tag = m.replaceAll("")
|
||||
}
|
||||
// Tag length limit was removed in API 26.
|
||||
return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
|
||||
tag
|
||||
} else {
|
||||
tag.substring(0, MAX_TAG_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KERMIT_LOGGER_DEFAULT_TAG = ""
|
||||
|
||||
/**
|
||||
* copy from [Timber.DebugTree.Companion]
|
||||
*/
|
||||
private const val MAX_TAG_LENGTH = 23
|
||||
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.tap.core
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class DefaultAppCoroutineScope @Inject constructor(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : AppCoroutineScope {
|
||||
|
||||
private val tag = "AppCoroutineScope"
|
||||
|
||||
override val coroutineContext: CoroutineContext = SupervisorJob() +
|
||||
// keep IO dispatcher to avoid blocking Default with IO operations
|
||||
dispatchers.io +
|
||||
CoroutineName(tag) +
|
||||
CoroutineExceptionHandler { context, throwable ->
|
||||
val coroutineName = context[CoroutineName]?.name.orEmpty()
|
||||
logError(throwable, coroutineName)
|
||||
}
|
||||
|
||||
private fun logError(throwable: Throwable, coroutineName: String) {
|
||||
Logger.withTag(tag).e(
|
||||
messageString = "CoroutineName $coroutineName",
|
||||
throwable = throwable,
|
||||
)
|
||||
val event = ExceptionAnalyticsEvent(
|
||||
exception = throwable,
|
||||
params = mapOf(
|
||||
"source" to tag,
|
||||
"coroutineName" to coroutineName,
|
||||
),
|
||||
)
|
||||
analyticsExceptionHandler.sendException(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
|
|||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -21,8 +22,6 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -62,8 +61,8 @@ internal object ActivityModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
@DelayedWork
|
||||
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
|
||||
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
fun provideActivityDelayedWorkCoroutineScope(appScope: AppCoroutineScope): CoroutineScope {
|
||||
return appScope
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.domain.card.BuildConfig
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
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.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -44,7 +44,7 @@ internal class TangemSdkManagerModule {
|
|||
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -62,7 +62,7 @@ internal class TangemSdkManagerModule {
|
|||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
dispatchers = dispatchers,
|
||||
coroutineScope = appScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
|
@ -30,6 +31,7 @@ internal object UiDependenciesModule {
|
|||
override val vibratorHapticManager = vibratorHapticManager
|
||||
override val appThemeModeHolder = appThemeModeHolder
|
||||
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
|
||||
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
|
||||
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
|
||||
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import com.tangem.core.navigation.finisher.AppFinisher
|
|||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.finisher.AndroidAppFinisher
|
||||
import com.tangem.tap.common.settings.IntentSettingsManager
|
||||
import com.tangem.tap.common.share.IntentShareManager
|
||||
import com.tangem.tap.common.url.CustomTabsUrlOpener
|
||||
import com.tangem.tap.core.DefaultAppCoroutineScope
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -18,21 +21,28 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object UtilsModule {
|
||||
internal interface UtilsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShareManager(): ShareManager = IntentShareManager()
|
||||
@Binds
|
||||
fun provideAppScope(defaultAppScope: DefaultAppCoroutineScope): AppCoroutineScope
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShareManager(): ShareManager = IntentShareManager()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager =
|
||||
IntentSettingsManager(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,14 +40,6 @@ internal object ManageTokensDomainModule {
|
|||
return FindTokenUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckIsCurrencyNotAddedUseCase(
|
||||
customTokensRepository: CustomTokensRepository,
|
||||
): CheckIsCurrencyNotAddedUseCase {
|
||||
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSupportedNetworksUseCase(
|
||||
|
|
@ -64,12 +56,6 @@ internal object ManageTokensDomainModule {
|
|||
return ValidateDerivationPathUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase {
|
||||
return CheckHasLinkedTokensUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.nft.utils.NFTCleaner
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -63,9 +62,7 @@ internal object NFTDomainModule {
|
|||
fun providesGetNFTAvailableNetworksUseCase(
|
||||
nftRepository: NFTRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
nftRepository = nftRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,4 +34,18 @@ internal object QrScanningDomainModule {
|
|||
fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase {
|
||||
return ParseQrCodeUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideResolveQrSendTargetsUseCase(
|
||||
multiAccountListSupplier: MultiAccountListSupplier,
|
||||
qrScanningEventsRepository: QrScanningEventsRepository,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
): ResolveQrSendTargetsUseCase {
|
||||
return ResolveQrSendTargetsUseCase(
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
qrScanningEventsRepository = qrScanningEventsRepository,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,20 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
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.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
|
@ -36,7 +31,6 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
internal object TokensDomainModule {
|
||||
|
||||
@Provides
|
||||
|
|
@ -53,67 +47,6 @@ internal object TokensDomainModule {
|
|||
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyUseCase(
|
||||
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetSingleCryptoCurrencyStatusUseCase {
|
||||
return GetSingleCryptoCurrencyStatusUseCase(
|
||||
currencyStatusOperations = baseCurrencyStatusOperations,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyWarningsUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): GetCurrencyWarningsUseCase {
|
||||
return GetCurrencyWarningsUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
dispatchers = dispatchers,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
currencyStatusOperations = baseCurrencyStatusOperations,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchCurrencyStatusUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchCurrencyStatusUseCase {
|
||||
return FetchCurrencyStatusUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrencyUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): GetCryptoCurrencyUseCase {
|
||||
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
|
|
@ -132,30 +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
|
||||
@Singleton
|
||||
fun provideGetMinimumTransactionAmountSyncUseCase(
|
||||
|
|
@ -239,37 +148,13 @@ internal object TokensDomainModule {
|
|||
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBaseCurrencyStatusOperations(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
|
||||
multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): BaseCurrencyStatusOperations {
|
||||
return BaseCurrencyStatusOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleStakingBalanceSupplier = singleStakingBalanceSupplier,
|
||||
multiStakingBalanceSupplier = multiStakingBalanceSupplier,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletBalanceFetcher(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesFetcher: MultiWalletCryptoCurrenciesFetcher,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
expressServiceFetcher: ExpressServiceFetcher,
|
||||
multiWalletAccountListFetcher: MultiWalletAccountListFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
|
|
@ -279,8 +164,10 @@ internal object TokensDomainModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletBalanceFetcher {
|
||||
return WalletBalanceFetcher(
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiWalletCryptoCurrenciesFetcher = multiWalletCryptoCurrenciesFetcher,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
multiWalletAccountListFetcher = multiWalletAccountListFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.data.wallets.hot.TangemHotWalletSigner
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
|
|
@ -19,13 +19,10 @@ import com.tangem.domain.transaction.WalletAddressServiceRepository
|
|||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.transaction.usecase.gasless.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
|
|
@ -56,8 +53,8 @@ internal object TransactionDomainModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
pushNotificationsRepository: PushNotificationsRepository,
|
||||
appScope: AppCoroutineScope,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
demoConfig = DemoConfig,
|
||||
|
|
@ -65,7 +62,7 @@ internal object TransactionDomainModule {
|
|||
transactionRepository = transactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.io),
|
||||
parallelUpdatingScope = appScope,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
pushNotificationsRepository = pushNotificationsRepository,
|
||||
)
|
||||
|
|
@ -279,14 +276,12 @@ internal object TransactionDomainModule {
|
|||
@Singleton
|
||||
fun provideGetAvailableFeeTokensUseCase(
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetAvailableFeeTokensUseCase {
|
||||
return GetAvailableFeeTokensUseCase(
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -296,17 +291,15 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForGaslessUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getFeeUseCase: GetFeeUseCase,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetFeeForGaslessUseCase {
|
||||
return GetFeeForGaslessUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
|
|
@ -317,16 +310,14 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): GetFeeForTokenUseCase {
|
||||
return GetFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -344,13 +335,13 @@ internal object TransactionDomainModule {
|
|||
fun provideCreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
getSingCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
): CreateAndSendGaslessTransactionUseCase {
|
||||
return CreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
getSingleCryptoCurrencyStatusUseCase = getSingCryptoCurrencyStatusUseCase,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
|
|
@ -362,16 +353,14 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForTokenUseCase {
|
||||
return EstimateFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -381,8 +370,7 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForGaslessTxUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
estimateFeeUseCase: EstimateFeeUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForGaslessTxUseCase {
|
||||
|
|
@ -390,8 +378,7 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
estimateFeeUseCase = estimateFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.error.FeeErrorResolver
|
||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
|
|
@ -15,8 +16,6 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -145,12 +144,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyMinAmountUseCase(
|
||||
feeRepository: FeeRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyMinAmountUseCase {
|
||||
return YieldSupplyMinAmountUseCase(
|
||||
feeRepository = feeRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +158,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyGetCurrentFeeUseCase(
|
||||
feeRepository: FeeRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyGetCurrentFeeUseCase {
|
||||
return YieldSupplyGetCurrentFeeUseCase(
|
||||
feeRepository = feeRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -173,12 +172,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyGetMaxFeeUseCase(
|
||||
yieldSupplyRepository: YieldSupplyRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
): YieldSupplyGetMaxFeeUseCase {
|
||||
return YieldSupplyGetMaxFeeUseCase(
|
||||
yieldSupplyRepository = yieldSupplyRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -253,12 +252,12 @@ internal object YieldSupplyDomainModule {
|
|||
fun provideYieldSupplyPendingProcessorUseCase(
|
||||
yieldSupplyRepository: YieldSupplyRepository,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
dispatcherProvider: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): YieldSupplyPendingTracker {
|
||||
return YieldSupplyPendingTracker(
|
||||
yieldSupplyRepository = yieldSupplyRepository,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.io),
|
||||
coroutineScope = appScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
|||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
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.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
|
@ -86,7 +86,7 @@ internal class DefaultTangemSdkManager(
|
|||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
|
@ -115,8 +115,6 @@ internal class DefaultTangemSdkManager(
|
|||
override val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io)
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
needEnrollBiometrics
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import com.tangem.core.decompose.di.GlobalUiMessageSender
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
import com.tangem.core.ui.components.bottomsheets.message.*
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
|
|
@ -216,30 +216,28 @@ internal class MainViewModel @Inject constructor(
|
|||
if (!settings.isUpdateFromToast) {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(false)
|
||||
|
||||
val message = BottomSheetMessage.invoke(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
onDismissRequest = ::onBottomSheetDismissed,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = false)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = true)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
val message = bottomSheetMessage {
|
||||
infoBlock {
|
||||
title = resourceReference(R.string.balance_hidden_title)
|
||||
body = resourceReference(R.string.balance_hidden_description)
|
||||
icon(R.drawable.ic_eye_off_outline_24)
|
||||
}
|
||||
onDismiss { onBottomSheetDismissed() }
|
||||
primaryButton {
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button)
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = false)
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button)
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = true)
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.tangem.data.card.TransactionSignerFactory
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
|
|
@ -71,7 +70,6 @@ data class DaggerGraphState(
|
|||
val uiMessageSender: UiMessageSender? = null,
|
||||
val cardArworksProvider: CardArtworksProvider? = null,
|
||||
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
|
||||
val userTokensResponseStore: UserTokensResponseStore? = null,
|
||||
val userWalletsListRepository: UserWalletsListRepository? = null,
|
||||
val tangemHotSdk: TangemHotSdk? = null,
|
||||
val trackingContextProxy: TrackingContextProxy? = null,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -28,9 +29,12 @@ import com.arkivanov.essenty.backhandler.BackHandler
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.LocalRootBackgroundColor
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.LocalTopSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.security.ProvideSecureFlagController
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
|
|
@ -93,6 +97,16 @@ internal fun RootContent(
|
|||
.padding(all = 16.dp),
|
||||
hostState = snackbarHostState,
|
||||
)
|
||||
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemTopSnackbarHost(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.statusBarsPadding()
|
||||
.padding(all = 16.dp),
|
||||
hostState = LocalTopSnackbarHostState.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMessageEffect()
|
||||
|
|
|
|||
|
|
@ -349,6 +349,7 @@ internal class ChildFactory @Inject constructor(
|
|||
val source = when (route.source) {
|
||||
is AppRoute.QrScanning.Source.Send -> SourceType.SEND
|
||||
is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT
|
||||
is AppRoute.QrScanning.Source.MainScreen -> SourceType.MAIN_SCREEN
|
||||
}
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
|||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import arrow.core.right
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -30,7 +30,7 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
}
|
||||
private val handler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ dependencies {
|
|||
/* Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.qrScanning.models)
|
||||
|
|
|
|||
|
|
@ -167,11 +167,14 @@ sealed class AppRoute(val path: String) : Route {
|
|||
get() = when (this) {
|
||||
is Send -> "/$networkName"
|
||||
WalletConnect -> ""
|
||||
MainScreen -> ""
|
||||
}
|
||||
|
||||
data class Send(val networkName: String) : Source()
|
||||
|
||||
data object WalletConnect : Source()
|
||||
|
||||
data object MainScreen : Source()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.common.routing
|
||||
|
||||
import android.net.Uri
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Routes in-app content links (deep links and external URLs)
|
||||
* - `tangem://` scheme → parsed to AppRoute and pushed via AppRouter
|
||||
* - `https://` / `http://` → opened in external browser via UrlOpener
|
||||
* - Unknown scheme → logged and ignored
|
||||
*/
|
||||
class LinkHandler(
|
||||
private val appRouter: AppRouter,
|
||||
) {
|
||||
|
||||
fun navigate(link: String) {
|
||||
val uri = Uri.parse(link)
|
||||
handleTangemDeepLink(uri)
|
||||
}
|
||||
|
||||
private fun handleTangemDeepLink(uri: Uri) {
|
||||
val route = parseDeepLinkToRoute(uri)
|
||||
if (route != null) {
|
||||
appRouter.push(route)
|
||||
} else {
|
||||
Timber.w("ContentLinkHandler: unrecognized tangem deep link: %s", uri)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter", "FunctionOnlyReturningConstant")
|
||||
private fun parseDeepLinkToRoute(uri: Uri): AppRoute? {
|
||||
// TODO [REDACTED_TASK_KEY] refactor deepling routing
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.common.test
|
||||
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TestAppCoroutineScope(override val coroutineContext: CoroutineContext = Dispatchers.Unconfined) : AppCoroutineScope {
|
||||
|
||||
constructor(testScope: TestScope) : this(testScope.coroutineContext)
|
||||
}
|
||||
|
|
@ -0,0 +1,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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,8 +38,8 @@ import com.tangem.utils.StringsSigns.MINUS
|
|||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
fun MarketsListItemV1(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContentV1(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
|
|
@ -50,7 +50,7 @@ fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onC
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
private fun MarketsListItemContentV1(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
|
|
@ -273,11 +273,11 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
|
|||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
MarketsListItemV1(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
MarketsListItemV1(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
|
|
@ -16,7 +16,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
|
|||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
ratingPosition = "1",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
|
|
@ -33,7 +33,7 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
|
|||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
ratingPosition = "2",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ import androidx.compose.animation.core.tween
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
|
|
@ -18,6 +22,23 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
*/
|
||||
@Composable
|
||||
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TokenPriceTextV2(
|
||||
price = price,
|
||||
modifier = modifier,
|
||||
priceChangeType = priceChangeType,
|
||||
)
|
||||
} else {
|
||||
TokenPriceTextV1(
|
||||
price = price,
|
||||
modifier = modifier,
|
||||
priceChangeType = priceChangeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV1(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
|
@ -52,4 +73,60 @@ fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType
|
|||
style = TangemTheme.typography.body2,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors2.text.status.accent
|
||||
val fallColor = TangemTheme.colors2.text.status.warning
|
||||
val generalColor = TangemTheme.colors2.text.neutral.primary
|
||||
val decimalColor = TangemTheme.colors2.text.neutral.secondary
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
var isAnimationSkipped by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(price) {
|
||||
if (!isAnimationSkipped) {
|
||||
isAnimationSkipped = true
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (priceChangeType != null) {
|
||||
val nextColor = when (priceChangeType) {
|
||||
PriceChangeType.UP -> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
PriceChangeType.NEUTRAL -> return@LaunchedEffect
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
}
|
||||
|
||||
val annotatedText = remember(price) {
|
||||
buildAnnotatedString {
|
||||
val dotIndex = price.indexOf(".")
|
||||
|
||||
if (dotIndex == -1) {
|
||||
append(price)
|
||||
} else {
|
||||
append(price.take(dotIndex))
|
||||
|
||||
withStyle(
|
||||
style = SpanStyle(color = decimalColor),
|
||||
) {
|
||||
append(price.substring(dotIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = annotatedText,
|
||||
color = color.value,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,13 +6,11 @@ import com.tangem.core.abtests.manager.ABTestsManager
|
|||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -24,7 +22,7 @@ internal object ABTestsManagerModule {
|
|||
fun provideABTestsManager(
|
||||
application: Application,
|
||||
environmentConfig: EnvironmentConfig,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): ABTestsManager {
|
||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||
StubABTestsManager()
|
||||
|
|
@ -32,7 +30,7 @@ internal object ABTestsManagerModule {
|
|||
AmplitudeABTestsManager(
|
||||
application = application,
|
||||
apiKey = environmentConfig.amplitudeApiKey,
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import com.amplitude.experiment.ExperimentConfig
|
|||
import com.amplitude.experiment.ExperimentUser
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
val apiKey: String,
|
||||
val scope: CoroutineScope,
|
||||
val scope: AppCoroutineScope,
|
||||
) : ABTestsManager {
|
||||
|
||||
private lateinit var client: ExperimentClient
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import org.json.JSONArray
|
||||
import com.tangem.plugin.configuration.configurations.TogglesGenerator
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -11,10 +9,31 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.squareup:kotlinpoet:1.15.0")
|
||||
classpath("org.json:json:20231013")
|
||||
abstract class GenerateTogglesTask : DefaultTask() {
|
||||
|
||||
@get:InputFiles
|
||||
abstract val configFiles: ConfigurableFileCollection
|
||||
|
||||
@get:Input
|
||||
abstract val fileNames: ListProperty<String>
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val files = configFiles.files.toList()
|
||||
val names = fileNames.get()
|
||||
|
||||
require(files.size == names.size) {
|
||||
"configFiles (${files.size}) and fileNames (${names.size}) must have the same size"
|
||||
}
|
||||
|
||||
files.zip(names).forEach { (inputFile, fileName) ->
|
||||
require(inputFile.exists()) { "Config file not found: ${inputFile.absolutePath}" }
|
||||
logger.lifecycle("Generating toggles from ${inputFile.name}")
|
||||
TogglesGenerator.generate(inputFile, outputDir.get().asFile, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,8 +42,20 @@ android {
|
|||
sourceSets["main"].java.srcDir("build/generated/source/toggles")
|
||||
}
|
||||
|
||||
/** Config file to generated enum class name mapping */
|
||||
val toggles = mapOf(
|
||||
file("src/main/assets/configs/feature_toggles_config.json") to "FeatureToggles",
|
||||
file("src/main/assets/configs/excluded_blockchains_config.json") to "ExcludedBlockchainToggles",
|
||||
)
|
||||
|
||||
val generateToggles = tasks.register<GenerateTogglesTask>("generateToggles") {
|
||||
configFiles.from(toggles.keys)
|
||||
fileNames.set(toggles.values.toList())
|
||||
outputDir.set(layout.buildDirectory.dir("generated/source/toggles"))
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
|
||||
dependsOn(generateToggles)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
|
|
@ -51,77 +82,4 @@ dependencies {
|
|||
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/feature_toggles_config.json",
|
||||
generatedFileName = "FeatureToggles",
|
||||
)
|
||||
}
|
||||
|
||||
val generateExcludedBlockchainToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json",
|
||||
generatedFileName = "ExcludedBlockchainToggles",
|
||||
)
|
||||
}
|
||||
|
||||
fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
||||
val inputFile = file(inputFilePath)
|
||||
val outputDir = file("build/generated/source/toggles")
|
||||
|
||||
inputs.file(inputFile)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val jsonText = inputFile.readText()
|
||||
val jsonArray = JSONArray(jsonText)
|
||||
|
||||
val entries = (0 until jsonArray.length()).map { i ->
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val name = obj.getString("name")
|
||||
val version = obj.getString("version")
|
||||
CodeBlock.of("%S to %S", name, version)
|
||||
}
|
||||
|
||||
val mapInitializer = CodeBlock.builder()
|
||||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEach { entry ->
|
||||
add(entry)
|
||||
add(",\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
.add(")")
|
||||
.build()
|
||||
|
||||
val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName)
|
||||
.addKdoc("Generated from $inputFilePath")
|
||||
.addProperty(
|
||||
PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING))
|
||||
.initializer(mapInitializer)
|
||||
.build()
|
||||
)
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName)
|
||||
.addType(objectBuilder.build())
|
||||
.build()
|
||||
|
||||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
|
||||
// Remove redundant public visibility modifiers
|
||||
val generatedFile = File(outputPackageDir, "com/tangem/core/configtoggle/$generatedFileName.kt")
|
||||
if (generatedFile.exists()) {
|
||||
val content = generatedFile.readText()
|
||||
val fixedContent = content
|
||||
.replace("public object ", "object ")
|
||||
.replace("public val ", "val ")
|
||||
generatedFile.writeText(fixedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,5 +59,13 @@
|
|||
{
|
||||
"name": "CUSTOMER_IO_ENABLED",
|
||||
"version": "5.35"
|
||||
},
|
||||
{
|
||||
"name": "MAIN_SCREEN_QR_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_PROMO_BANNERS_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
|
|
@ -22,7 +20,7 @@ import kotlinx.coroutines.flow.*
|
|||
internal class DevApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appScope: AppCoroutineScope,
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
|
@ -51,7 +49,7 @@ internal class DevApiConfigsManager(
|
|||
|
||||
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
|
||||
}
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,22 @@ import com.tangem.datasource.api.common.config.ApiConfig
|
|||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.Boolean
|
||||
import kotlin.String
|
||||
import kotlin.Unit
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.any
|
||||
import kotlin.collections.associateWith
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.first
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.mapValues
|
||||
import kotlin.collections.plus
|
||||
import kotlin.error
|
||||
import kotlin.to
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in MOCK environment
|
||||
|
|
@ -18,7 +30,7 @@ import kotlinx.coroutines.flow.*
|
|||
*/
|
||||
internal class MockApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
|
@ -26,8 +38,6 @@ internal class MockApiConfigsManager(
|
|||
|
||||
override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ interface TangemExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromAmount") fromAmount: String?,
|
||||
@Query("toAmount") toAmount: String? = null,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
|
|
@ -57,7 +58,8 @@ interface TangemExpressApi {
|
|||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("fromAddress") fromAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromAmount") fromAmount: String?,
|
||||
@Query("toAmount") toAmount: String? = null,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
|
|
|
|||
|
|
@ -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.StoryContentResponse
|
||||
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.GetWalletArchivedAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
|
|
@ -42,15 +45,6 @@ interface TangemTechApi {
|
|||
@GET("v1/geo")
|
||||
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")
|
||||
suspend fun saveTokens(
|
||||
@Path(value = "walletId") userId: String,
|
||||
|
|
@ -135,14 +129,8 @@ interface TangemTechApi {
|
|||
@PATCH("v1/user-wallets/wallets/{wallet_id}")
|
||||
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")
|
||||
suspend fun associateApplicationIdWithWalletsV2(
|
||||
suspend fun associateApplicationIdWithWallets(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: AssociateApplicationIdWithWalletsBody,
|
||||
): ApiResponse<Unit>
|
||||
|
|
@ -197,6 +185,21 @@ interface TangemTechApi {
|
|||
): ApiResponse<PromoBannerV2Response>
|
||||
// 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
|
||||
* notifications for yield operations (deposit, withdraw, send).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.datasource.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.preferences.*
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -20,10 +21,11 @@ internal object AppPreferencesStoreModule {
|
|||
fun provideAppPreferencesStore(
|
||||
@ApplicationContext appContext: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
@SdkMoshi moshi: Moshi,
|
||||
): AppPreferencesStore {
|
||||
return AppPreferencesStore(
|
||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, appScope = appScope),
|
||||
moshi = moshi,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ import com.tangem.datasource.local.token.ExpressAssetsStore
|
|||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -30,7 +28,7 @@ internal object ExpressAssetsStoreModule {
|
|||
fun provideExpressAssetsStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): ExpressAssetsStore {
|
||||
return DefaultExpressAssetsStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
|
|
@ -40,7 +38,7 @@ internal object ExpressAssetsStoreModule {
|
|||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile("express_assets") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
),
|
||||
runtimeStore = RuntimeDataStore(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.Timeouts
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -47,11 +47,11 @@ internal object NetworkModule {
|
|||
fun provideApiConfigManager(
|
||||
apiConfigs: ApiConfigs,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): ApiConfigsManager {
|
||||
return when {
|
||||
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, dispatchers)
|
||||
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
|
||||
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, appScope)
|
||||
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, appScope)
|
||||
else -> ProdApiConfigsManager(apiConfigs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
|||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -39,7 +37,7 @@ internal object StakingStoreModule {
|
|||
fun provideStakingTokensStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): StakingYieldsStore {
|
||||
return DefaultStakingYieldsStore(
|
||||
dataStore = DataStoreFactory.create(
|
||||
|
|
@ -49,7 +47,7 @@ internal object StakingStoreModule {
|
|||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "yields_cache") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -59,7 +57,7 @@ internal object StakingStoreModule {
|
|||
fun provideYieldsBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): DataStore<Map<String, Set<YieldBalanceWrapperDTO>>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
|
|
@ -68,7 +66,7 @@ internal object StakingStoreModule {
|
|||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "yield_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +81,7 @@ internal object StakingStoreModule {
|
|||
fun provideP2PEthPoolBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
|
|
@ -92,7 +90,7 @@ internal object StakingStoreModule {
|
|||
defaultValue = emptyMap(),
|
||||
),
|
||||
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(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): P2PEthPoolVaultsStore {
|
||||
return DefaultP2PEthPoolVaultsStore(
|
||||
dataStore = DataStoreFactory.create(
|
||||
|
|
@ -111,7 +109,7 @@ internal object StakingStoreModule {
|
|||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ import com.tangem.datasource.local.token.DefaultTokenReceiveWarningActionStore
|
|||
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -27,7 +25,7 @@ object TokenReceiveWarningModule {
|
|||
fun provideTokenReceiveWarningStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): TokenReceiveWarningActionStore {
|
||||
return DefaultTokenReceiveWarningActionStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
|
|
@ -37,7 +35,7 @@ object TokenReceiveWarningModule {
|
|||
defaultValue = emptySet(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ import com.tangem.datasource.local.walletconnect.DefaultWalletConnectStore
|
|||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
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.WcSessionDTO
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -29,9 +27,8 @@ object WalletConnectModule {
|
|||
fun provideWalletConnectStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
scope: AppCoroutineScope,
|
||||
): WalletConnectStore {
|
||||
val scope = CoroutineScope(context = dispatchers.io + SupervisorJob())
|
||||
return DefaultWalletConnectStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
|
|||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -28,7 +26,7 @@ object YieldSupplyModule {
|
|||
fun provideYieldMarketsStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appScope: AppCoroutineScope,
|
||||
): YieldMarketsStore {
|
||||
return DefaultYieldMarketsStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
|
|
@ -38,7 +36,7 @@ object YieldSupplyModule {
|
|||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.local.logs
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -27,13 +28,9 @@ import javax.inject.Singleton
|
|||
class AppLogsStore @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
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 zipMutex = Mutex()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ import com.tangem.datasource.di.NetworkMoshi
|
|||
import com.tangem.datasource.local.nft.custom.NFTPriceId
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -24,7 +22,7 @@ import javax.inject.Singleton
|
|||
class NFTPersistenceStoreFactory @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
|
||||
|
|
@ -61,7 +59,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
|||
defaultValue = defaultValue,
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = fileName) },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
private fun Network.ID.formatted(): String = rawId.value
|
||||
|
|
|
|||
|
|
@ -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.utils.CleanupKeyMigration
|
||||
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Application preferences data store 'DataStore<Preferences>'.
|
||||
|
|
@ -35,15 +33,15 @@ internal object PreferencesDataStore {
|
|||
|
||||
private var INSTANCE: DataStore<Preferences>? = null
|
||||
|
||||
fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
|
||||
return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it }
|
||||
fun getInstance(context: Context, appScope: AppCoroutineScope): DataStore<Preferences> {
|
||||
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(
|
||||
corruptionHandler = createCorruptionHandler(),
|
||||
migrations = createMigrations(context = context),
|
||||
scope = CoroutineScope(context = dispatcher + SupervisorJob()),
|
||||
scope = appScope,
|
||||
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -217,6 +217,7 @@
|
|||
<string name="common_add">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_tokens">Token hinzufügen</string>
|
||||
<string name="common_added">Hinzugefügt</string>
|
||||
<string name="common_address">Vertragsadresse</string>
|
||||
<string name="common_all">Alle</string>
|
||||
|
|
@ -399,6 +400,7 @@
|
|||
<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_value_copied">Wert kopiert</string>
|
||||
<string name="common_wallets">Meine Wallet</string>
|
||||
<string name="common_week">Woche</string>
|
||||
<string name="common_with">mit</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="earn_best_opportunities">Beste Gelegenheiten</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_types">Alle Arten</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_no_results">Keine Ergebnisse</string>
|
||||
<string name="earn_title">Verdienen</string>
|
||||
|
|
@ -826,6 +831,7 @@
|
|||
<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_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_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>
|
||||
|
|
@ -845,6 +851,7 @@
|
|||
<string name="news_related_tokens">Verwandte Token</string>
|
||||
<string name="news_sources">Verwandte Nachrichten</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="nft_about_title">Über NFT</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_seed_button_read_more">Lese mehr über die Seed-Phrase</string>
|
||||
<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>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_generate_title">Deine Seed-Phrase</string>
|
||||
<plurals name="onboarding_seed_generate_words_count">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="one">.</item>
|
||||
<item quantity="other">%d Wörter</item>
|
||||
</plurals>
|
||||
<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="other">für %d Wallets</item>
|
||||
</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_title">Du</string>
|
||||
<string name="referral_point_discount_description_prefix">Bekommt eine</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 erhältst</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_value">%s Rabatt</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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
<string name="common_add">Agregar</string>
|
||||
<string name="common_add_to_portfolio">Añadir al portafolio</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_address">Dirección</string>
|
||||
<string name="common_all">Todos</string>
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
<string name="common_add">Ajouter</string>
|
||||
<string name="common_add_to_portfolio">Ajouter au portfolio</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_address">Adresse</string>
|
||||
<string name="common_all">Tous</string>
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@
|
|||
<string name="common_add">追加</string>
|
||||
<string name="common_add_to_portfolio">ポートフォリオに追加</string>
|
||||
<string name="common_add_token">トークンを追加</string>
|
||||
<string name="common_add_tokens">トークンの追加</string>
|
||||
<string name="common_added">追加済み</string>
|
||||
<string name="common_address">アドレス</string>
|
||||
<string name="common_all">すべて</string>
|
||||
|
|
@ -692,6 +693,7 @@
|
|||
<string name="koinos_mana_level_title">Manaレベル</string>
|
||||
<string name="main_empty_tokens_list_message">暗号資産および取引の追跡を開始するには、トークンを追加してください</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_title">カードまたはリングをスキャンする</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_sources">関連ニュース</string>
|
||||
<string name="news_stay_in_the_loop">最新情報を入手</string>
|
||||
<string name="news_trending_score">トレンドスコア</string>
|
||||
<string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string>
|
||||
<string name="nft_about_title">NFTについて</string>
|
||||
<string name="nft_asset">NFTアセット</string>
|
||||
|
|
@ -1488,6 +1491,7 @@
|
|||
<string name="tangempay_cancel_kyc">メイン画面からKYCを非表示にする</string>
|
||||
<string name="tangempay_card_details_add_funds">資金を追加</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_change_pin">PINを変更する</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_too_maximum_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_failed_to_verify_card_message">このカードは、サンプル品または偽造品である可能性があります</string>
|
||||
<string name="warning_failed_to_verify_card_title">真正性チェックに失敗しました</string>
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@
|
|||
<string name="common_add">Добавить</string>
|
||||
<string name="common_add_to_portfolio">Добавить в портфель</string>
|
||||
<string name="common_add_token">Добавить токен</string>
|
||||
<string name="common_add_tokens">Добавьте токены</string>
|
||||
<string name="common_added">Добавлен</string>
|
||||
<string name="common_address">Адрес</string>
|
||||
<string name="common_all">Все</string>
|
||||
|
|
@ -408,6 +409,7 @@
|
|||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_understand_continue">Я понимаю, продолжить</string>
|
||||
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
||||
<string name="common_unlock">Разблокировать</string>
|
||||
<string name="common_unreachable">Недоступно</string>
|
||||
<string name="common_unstake">Завершить стейкинг</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_search_placeholder">Например Bitcoin</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_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string>
|
||||
<string name="markets_about_coin_header">О монете</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_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_apy_placeholder">APY %s</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_24h_description">Общая сумма криптовалюты, которая была продана за последние 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_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string>
|
||||
<string name="markets_tooltip_title">Добавить токены</string>
|
||||
|
|
@ -1499,7 +1502,7 @@
|
|||
<string name="swapping_swap_action_in_progress">Обмен…</string>
|
||||
<string name="swapping_to_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_title">Tangem Pay в режиме beta</string>
|
||||
<string name="tangem_pay_card_frozen">Карта заморожена</string>
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@
|
|||
<string name="common_add">Додати</string>
|
||||
<string name="common_add_to_portfolio">Додати у портфель</string>
|
||||
<string name="common_add_token">Додати токен</string>
|
||||
<string name="common_add_tokens">Додайте токени</string>
|
||||
<string name="common_added">Додано</string>
|
||||
<string name="common_address">Адреса</string>
|
||||
<string name="common_all">Усе</string>
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
<string name="common_add">Add</string>
|
||||
<string name="common_add_to_portfolio">Add to portfolio</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_address">Address</string>
|
||||
<string name="common_all">All</string>
|
||||
|
|
@ -325,6 +326,7 @@
|
|||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">No</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_available">Not available</string>
|
||||
<string name="common_not_now">Not now</string>
|
||||
|
|
@ -383,6 +385,7 @@
|
|||
<string name="common_to">To</string>
|
||||
<string name="common_to_wallet_name">To %s</string>
|
||||
<string name="common_today">Today</string>
|
||||
<string name="common_token_send">Token to send</string>
|
||||
<plurals name="common_tokens_count">
|
||||
<item quantity="one">%d token</item>
|
||||
<item quantity="other">%d tokens</item>
|
||||
|
|
@ -395,6 +398,7 @@
|
|||
<string name="common_understand">I understand</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_unlock">Unlock</string>
|
||||
<string name="common_unreachable">Unreachable</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>
|
||||
|
|
@ -699,6 +703,7 @@
|
|||
<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_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_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>
|
||||
|
|
@ -736,6 +741,8 @@
|
|||
<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_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 section’s 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_no_data_title">No data</string>
|
||||
|
|
@ -798,6 +805,10 @@
|
|||
<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_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_links">Links</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_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_trading_interval">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_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_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>
|
||||
|
|
@ -1202,6 +1215,10 @@
|
|||
<string name="send_memo">Memo: %s</string>
|
||||
<string name="send_memo_destination_tag_error">Invalid Memo</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_footer">Unique number for each transaction. Use it to resend or cancel a pending transaction.</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_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_to_wallet_button_text">Add to Google Wallet</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_success_description">The card is fully ready for payments.</string>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ dependencies {
|
|||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.decompose)
|
||||
api(projects.core.decompose)
|
||||
implementation(projects.core.error)
|
||||
|
||||
/** AndroidX libraries */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
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.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
|
@ -15,6 +16,8 @@ interface UiDependencies {
|
|||
|
||||
val globalSnackbarHostState: SnackbarHostState
|
||||
|
||||
val globalTopSnackbarHostState: TangemTopSnackbarHostState
|
||||
|
||||
val eventMessageHandler: EventMessageHandler
|
||||
|
||||
val designFeatureToggles: DesignFeatureToggles
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
* but with a horizontal gradient.
|
||||
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@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(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size100 + bottomBarHeight)
|
||||
.background(gradientBrush),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -84,8 +80,11 @@ fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
|
|||
backgroundColor = Color.Transparent,
|
||||
),
|
||||
) {
|
||||
progressive =
|
||||
HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f)
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = 0f,
|
||||
endIntensity = 1f,
|
||||
preferPerformance = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -114,7 +113,11 @@ fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier
|
|||
),
|
||||
) {
|
||||
progressive =
|
||||
HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f)
|
||||
HazeProgressive.horizontalGradient(
|
||||
startIntensity = 0f,
|
||||
endIntensity = 1f,
|
||||
preferPerformance = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,18 @@ import androidx.compose.ui.geometry.Rect
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
||||
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(
|
||||
initialValue = Color(0xFF3355EE),
|
||||
targetValue = Color(0xFF5577FF),
|
||||
initialValue = if (isDark) Color(0xFF0D0D3A) else Color(0xFFCCB8EE),
|
||||
targetValue = if (isDark) Color(0xFF1C1C6E) else Color(0xFFBBA0E8),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(4_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
|
|
@ -51,10 +53,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
|||
label = "y1",
|
||||
)
|
||||
|
||||
// ── Circle 2 (right) ─────────────────────────────────────────────────────
|
||||
// ── Circle 2 (right) — matches dc2 / lc2 from shader version ────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF7733CC),
|
||||
targetValue = Color(0xFF4455EE),
|
||||
initialValue = if (isDark) Color(0xFF0A1238) else Color(0xFFB8C8F0),
|
||||
targetValue = if (isDark) Color(0xFF112266) else Color(0xFF9AAEE8),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
|
|
@ -82,10 +84,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
|||
label = "y2",
|
||||
)
|
||||
|
||||
// ── Oval (center) ────────────────────────────────────────────────────────
|
||||
// ── Oval (center) — matches dc4 / lc4 from shader version ───────────────
|
||||
val ovalColor by transition.animateColor(
|
||||
initialValue = Color(0xFF5533CC),
|
||||
targetValue = Color(0xFF8844EE),
|
||||
initialValue = if (isDark) Color(0xFF081A30) else Color(0xFFB8C4EE),
|
||||
targetValue = if (isDark) Color(0xFF113355) else Color(0xFFA8B4E8),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
|
|
@ -93,10 +95,10 @@ internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
|||
),
|
||||
label = "ovalColor",
|
||||
)
|
||||
// ── Circle 3 (center) ────────────────────────────────────────────────────
|
||||
// ── Circle 3 (center) — matches dc3 / lc3 from shader version ───────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF9933BB),
|
||||
targetValue = Color(0xFFBB44DD),
|
||||
initialValue = if (isDark) Color(0xFF110A38) else Color(0xFFDDC8F5),
|
||||
targetValue = if (isDark) Color(0xFF2A1666) else Color(0xFFCCB0EE),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.shader.NorthernLightsMeshGradientShader
|
||||
|
||||
|
|
@ -36,99 +37,58 @@ fun NorthernLightsBackground(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "NamedArguments")
|
||||
@Composable
|
||||
private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
|
||||
val isDark = LocalIsInDarkTheme.current
|
||||
|
||||
// Each track cycles through 4 states (matching the screenshot frames):
|
||||
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
|
||||
// 16 s total per track, staggered so no two tracks peak simultaneously.
|
||||
|
||||
// ── Color 1 – indigo → bright blue → lavender → hot violet ──────────────
|
||||
val color1 by transition.animateColor(
|
||||
initialValue = Color(0xFF2A1480),
|
||||
targetValue = Color(0xFF2A1480),
|
||||
@Composable
|
||||
fun anim(a: Color, b: Color, c: Color, d: Color, offset: Int = 0) = transition.animateColor(
|
||||
initialValue = a,
|
||||
targetValue = a,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF2A1480) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing
|
||||
durationMillis = 40_000
|
||||
a at 0 using FastOutSlowInEasing
|
||||
b at 10_000 using FastOutSlowInEasing
|
||||
c at 20_000 using FastOutSlowInEasing
|
||||
d at 30_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(offset),
|
||||
),
|
||||
label = "color1",
|
||||
label = "c$offset",
|
||||
)
|
||||
|
||||
// ── Color 2 – dark blue → cyan-blue → sky → teal ─────────────────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF1444AA),
|
||||
targetValue = Color(0xFF1444AA),
|
||||
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",
|
||||
)
|
||||
val dc1 by anim(Color(0xFF0D0D3A), Color(0xFF141455), Color(0xFF1C1C6E), Color(0xFF111148), 0)
|
||||
val dc2 by anim(Color(0xFF0A1238), Color(0xFF0D1D55), Color(0xFF112266), Color(0xFF0E1A4A), 10_000)
|
||||
val dc3 by anim(Color(0xFF110A38), Color(0xFF1C1050), Color(0xFF2A1666), Color(0xFF180E48), 20_000)
|
||||
val dc4 by anim(Color(0xFF081A30), Color(0xFF0D2844), Color(0xFF113355), Color(0xFF0D2240), 5_000)
|
||||
|
||||
// ── Color 3 – dark purple → medium purple → rose pink → magenta ──────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF4422BB),
|
||||
targetValue = Color(0xFF4422BB),
|
||||
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",
|
||||
)
|
||||
val lc1 by anim(Color(0xFFCCB8EE), Color(0xFFBBA0E8), Color(0xFFCCB0F0), Color(0xFFC4AAEC), 0)
|
||||
val lc2 by anim(Color(0xFFB8C8F0), Color(0xFF9AAEE8), Color(0xFFAABEF0), Color(0xFFA0B8EE), 10_000)
|
||||
val lc3 by anim(Color(0xFFDDC8F5), Color(0xFFCCB0EE), Color(0xFFD8BEF5), Color(0xFFD0B8F2), 20_000)
|
||||
val lc4 by anim(Color(0xFFB8C4EE), Color(0xFFA8B4E8), Color(0xFFB4C0EE), Color(0xFFAABCEC), 5_000)
|
||||
|
||||
// ── Color 4 – dark violet → medium violet → light pink → hot pink ────────
|
||||
val color4 by transition.animateColor(
|
||||
initialValue = Color(0xFF331199),
|
||||
targetValue = Color(0xFF331199),
|
||||
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",
|
||||
)
|
||||
val color1 = if (isDark) dc1 else lc1
|
||||
val color2 = if (isDark) dc2 else lc2
|
||||
val color3 = if (isDark) dc3 else lc3
|
||||
val color4 = if (isDark) dc4 else lc4
|
||||
|
||||
// Keep a stable shader instance so the RuntimeShader is never recreated.
|
||||
// Colors are pushed each recomposition via updateColors().
|
||||
val shader = remember {
|
||||
NorthernLightsMeshGradientShader(
|
||||
colors = arrayOf(
|
||||
Color(0xFF2A1480),
|
||||
Color(0xFF1444AA),
|
||||
Color(0xFF4422BB),
|
||||
Color(0xFF331199),
|
||||
color1,
|
||||
color2,
|
||||
color3,
|
||||
color4,
|
||||
containerColor,
|
||||
),
|
||||
speed = 0.5f,
|
||||
scale = 4f,
|
||||
speed = 0.07f,
|
||||
scale = 1.8f,
|
||||
)
|
||||
}
|
||||
val colorsArray = remember { Array(5) { Color.Unspecified } }
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ inline fun <T : Any> InformationBlockContentScope.ListItems(
|
|||
}
|
||||
|
||||
@Composable
|
||||
inline fun <T : Any> InformationBlockContentScope.GridItems(
|
||||
inline fun <T : Any> GridItems(
|
||||
items: ImmutableList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -48,17 +45,9 @@ fun ModalBottomSheetWithBackHandling(
|
|||
),
|
||||
content = {
|
||||
content()
|
||||
|
||||
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
|
||||
onBack?.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
|
||||
coroutineScope {
|
||||
launch { hide() }.invokeOnCompletion { onCollapsed() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +1,293 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.message
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.res.painterResource
|
||||
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.util.fastForEach
|
||||
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.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.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
fun MessageBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(config) { notification: MessageBottomSheetUM ->
|
||||
Content(model = notification)
|
||||
fun MessageBottomSheet(state: MessageBottomSheetUM, onDismissRequest: () -> Unit) {
|
||||
val stateWithOnDismiss = remember(state) {
|
||||
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
|
||||
internal fun Content(model: MessageBottomSheetUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(40.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(modifier = Modifier)
|
||||
|
||||
if (model.iconResId != null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(48.dp),
|
||||
painter = painterResource(model.iconResId),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
private fun ContentContainer(state: MessageBottomSheetUM.InfoBlock, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
BottomSheetIconContainer(state.icon, state.iconImage)
|
||||
state.title?.let { title ->
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing24),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
state.body?.let { body ->
|
||||
Text(
|
||||
text = model.message.resolveReference(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
text = body.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.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,
|
||||
)
|
||||
}
|
||||
state.chip?.let { chip ->
|
||||
BottomSheetChip(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing16),
|
||||
chip = chip,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_NotificationBottomSheet(
|
||||
@PreviewParameter(MessageBottomSheetUMPreviewProvider::class) params: MessageBottomSheetUM,
|
||||
private fun BottomSheetIconContainer(
|
||||
icon: MessageBottomSheetUM.Icon?,
|
||||
iconImage: MessageBottomSheetUM.IconImage?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
MessageBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
content = params,
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
),
|
||||
if (icon != null) {
|
||||
BottomSheetIcon(icon, modifier)
|
||||
} else if (iconImage != null) {
|
||||
Image(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size56)
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = iconImage.res),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageBottomSheetUMPreviewProvider : PreviewParameterProvider<MessageBottomSheetUM> {
|
||||
val balancesHiddenMessage = MessageBottomSheetUM(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
primaryAction = MessageBottomSheetUM.ActionUM(
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {},
|
||||
),
|
||||
secondaryAction = MessageBottomSheetUM.ActionUM(
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
@Composable
|
||||
private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) {
|
||||
val tint = when (icon.type) {
|
||||
MessageBottomSheetUM.Icon.Type.Unspecified -> Color.Unspecified
|
||||
MessageBottomSheetUM.Icon.Type.Accent -> TangemTheme.colors.icon.accent
|
||||
MessageBottomSheetUM.Icon.Type.Informative -> TangemTheme.colors.icon.informative
|
||||
MessageBottomSheetUM.Icon.Type.Attention -> TangemTheme.colors.icon.attention
|
||||
MessageBottomSheetUM.Icon.Type.Warning -> TangemTheme.colors.icon.warning
|
||||
}
|
||||
|
||||
override val values: Sequence<MessageBottomSheetUM>
|
||||
get() = sequenceOf(
|
||||
balancesHiddenMessage,
|
||||
balancesHiddenMessage.copy(title = null),
|
||||
balancesHiddenMessage.copy(iconResId = null),
|
||||
balancesHiddenMessage.copy(primaryAction = null),
|
||||
balancesHiddenMessage.copy(secondaryAction = null),
|
||||
balancesHiddenMessage.copy(
|
||||
primaryAction = null,
|
||||
secondaryAction = null,
|
||||
),
|
||||
balancesHiddenMessage.copy(
|
||||
title = null,
|
||||
iconResId = null,
|
||||
primaryAction = null,
|
||||
secondaryAction = null,
|
||||
),
|
||||
)
|
||||
val backgroundColor = when (icon.backgroundType) {
|
||||
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> TangemTheme.colors.icon.informative
|
||||
MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint
|
||||
MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors.icon.accent
|
||||
MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors.icon.informative
|
||||
MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors.icon.attention
|
||||
MessageBottomSheetUM.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
|
||||
}
|
||||
|
||||
HighlightedIcon(
|
||||
modifier = modifier,
|
||||
icon = icon.res,
|
||||
iconTint = tint,
|
||||
backgroundColor = backgroundColor,
|
||||
)
|
||||
}
|
||||
// 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,143 @@
|
|||
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 MessageBottomSheetUM(
|
||||
@DrawableRes val iconResId: Int?,
|
||||
val title: TextReference?,
|
||||
val message: TextReference,
|
||||
val primaryAction: ActionUM?,
|
||||
val secondaryAction: ActionUM?,
|
||||
var elements: ImmutableList<Element> = persistentListOf(),
|
||||
var onDismissRequest: () -> Unit = {},
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class ActionUM(
|
||||
val text: TextReference,
|
||||
val isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
@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 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
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue