diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a1514e89d5..9644d056cb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -73,15 +73,6 @@ android {
}
}
- buildTypes {
- debug {
- buildConfigField("String", "BUILD_TYPE", "\"debug\"")
- }
- release {
- buildConfigField("String", "BUILD_TYPE", "\"release\"")
- }
- }
-
}
configurations.all {
@@ -152,6 +143,7 @@ dependencies {
implementation(projects.domain.manageTokens)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
+ implementation(projects.domain.offramp)
implementation(projects.domain.onramp)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
@@ -304,6 +296,8 @@ dependencies {
implementation(projects.features.tokenRecieve.impl)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.yieldSupply.impl)
+ implementation(projects.features.approval.api)
+ implementation(projects.features.approval.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@@ -380,6 +374,8 @@ dependencies {
implementation(deps.amplitude)
implementation(deps.appsflyer)
implementation(deps.appsflyer.oaid)
+ implementation(deps.customerio.analytics)
+ implementation(deps.customerio.messaging)
implementation("com.android.installreferrer:installreferrer:2.2")
implementation(deps.spongecastle.core)
implementation(deps.lottie)
diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
index a7aa7666e3..cb51a8d6be 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
@@ -146,12 +146,8 @@ abstract class BaseTestCase : TestCase(
return ApplicationInjectionExecutionRule(
toggleStates = mapOf(
"SWAP_REDESIGN_ENABLED" to false,
- "NEW_ONRAMP_MAIN_ENABLED" to true,
- "HOT_WALLET_ENABLED" to true,
- "YIELD_SUPPLY_FEATURE_ENABLED" to true,
"ACCOUNTS_FEATURE_ENABLED" to true,
- "FEED_ENABLED" to true,
- "GASLESS_TRANSACTIONS_ENABLED" to true,
+ "GASLESS_APPROVAL_ENABLED" to true,
)
)
}
diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt
index 004e367549..b91b4767eb 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt
@@ -18,3 +18,19 @@ fun KNode.assertTextContainsSafe(
)
}
+fun KNode.isDisplayedSafely(): Boolean {
+ return try {
+ assertIsDisplayed()
+ true
+ } catch (_: AssertionError) {
+ false
+ }
+}
+
+fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
+ if (shouldBeDisplayed) {
+ assertIsDisplayed()
+ } else {
+ assertIsNotDisplayed()
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt
index bc4f4593d7..09f4b7bd1e 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt
@@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
-import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.utils.StringsSigns.DASH_SIGN
@@ -14,7 +13,6 @@ import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.scanCard(
productType: ProductType? = null,
mockContent: MockContent? = null,
- alreadyActivatedDialogIsShown: Boolean = false,
isTwinsCard: Boolean = false,
) {
if (productType != null) {
@@ -32,12 +30,6 @@ fun BaseTestCase.scanCard(
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
- if (alreadyActivatedDialogIsShown) {
- step("Click on 'This is my wallet' button") {
- waitForIdle()
- AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() }
- }
- }
if (isTwinsCard) {
step("Click on 'Continue' button") {
onOnboardingScreen { continueButton.clickWithAssertion() }
@@ -51,14 +43,12 @@ fun BaseTestCase.scanCard(
fun BaseTestCase.openMainScreen(
productType: ProductType? = null,
mockContent: MockContent? = null,
- alreadyActivatedDialogIsShown: Boolean = false,
isTwinsCard: Boolean = false,
) {
step("Scan card") {
scanCard(
productType = productType,
mockContent = mockContent,
- alreadyActivatedDialogIsShown = alreadyActivatedDialogIsShown,
isTwinsCard = isTwinsCard,
)
}
diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt
index f90275a934..7abb486367 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt
@@ -1,8 +1,8 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
+import com.tangem.common.extensions.assertVisibility
import com.tangem.screens.onSendConfirmScreen
-import io.github.kakaocup.compose.node.element.KNode
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkSendWarning(
@@ -45,12 +45,4 @@ fun BaseTestCase.checkSendWarning(
sendButton.assertIsEnabled()
}
}
-}
-
-private fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
- if (shouldBeDisplayed) {
- assertIsDisplayed()
- } else {
- assertIsNotDisplayed()
- }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt
index a86ca56854..358b29b5b3 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt
@@ -2,7 +2,9 @@ package com.tangem.scenarios
import androidx.compose.ui.test.click
import com.tangem.common.BaseTestCase
+import com.tangem.common.extensions.assertVisibility
import com.tangem.common.extensions.clickWithAssertion
+import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.screens.*
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
@@ -170,6 +172,131 @@ fun BaseTestCase.checkStoriesChanges() {
}
}
+fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
+ step("Click on 'Select fee' icon") {
+ onSwapTokenScreen { selectFeeIcon.performClick() }
+ }
+
+ when (feeType) {
+ FeeType.Market -> {
+ step("Click on 'Market' item") {
+ onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() }
+ }
+ step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
+ onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
+ }
+ }
+ FeeType.Fast -> {
+ step("Click on 'Fast' item") {
+ onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() }
+ }
+ step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
+ onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
+ }
+ }
+ }
+}
+
+fun BaseTestCase.selectFeeTypeWithGasless(feeType: FeeType, selectedFeeAmount: String) {
+ step("Click on 'Select fee' icon") {
+ onSwapTokenScreen { selectFeeIcon.performClick() }
+ }
+
+ when (feeType) {
+ FeeType.Market -> selectMarketFee(selectedFeeAmount)
+ FeeType.Fast -> selectFastFee(selectedFeeAmount)
+ }
+}
+
+private fun BaseTestCase.selectMarketFee(selectedFeeAmount: String) {
+ step("Deselect current fee and select 'Market'") {
+ onSwapSelectNetworkFeeBottomSheet {
+ if (fastSelectorItem.isDisplayedSafely()) {
+ fastSelectorItem.performClick()
+ }
+ marketSelectorItem.performClick()
+ }
+ }
+ step("Click on 'Apply' button") {
+ onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() }
+ }
+ step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
+ onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
+ }
+}
+
+private fun BaseTestCase.selectFastFee(selectedFeeAmount: String) {
+ step("Deselect current fee and select 'Fast'") {
+ onSwapSelectNetworkFeeBottomSheet {
+ if (marketSelectorItem.isDisplayedSafely()) {
+ marketSelectorItem.performClick()
+ }
+ fastSelectorItem.performClick()
+ }
+ }
+ step("Click on 'Apply' button") {
+ onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() }
+ }
+ step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
+ onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
+ }
+}
+
+fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) {
+ step("Assert 'Unable to cover '$networkName' fee notification title is displayed'") {
+ onSwapTokenScreen { unableToCoverFeeNotificationTitle(networkName).assertIsDisplayed() }
+ }
+ step("Assert 'Unable to cover '$networkName' fee notification text is displayed'") {
+ onSwapTokenScreen {
+ unableToCoverFeeNotificationText(
+ currencyName = networkName,
+ currencySymbol = currencySymbol
+ ).assertIsDisplayed()
+ }
+ }
+ step("Assert 'Unable to cover '$networkName' fee notification icon is displayed'") {
+ onSwapTokenScreen { unableToCoverFeeNotificationIcon(networkName).assertIsDisplayed() }
+ }
+}
+
+fun BaseTestCase.checkSwapWarning(
+ title: String,
+ message: String,
+ isDisplayed: Boolean = true,
+ swapButtonIsDisabled: Boolean = isDisplayed,
+) {
+ val assertDisplay = if (isDisplayed) "displayed" else "not displayed"
+
+ step("Assert warning title is $assertDisplay") {
+ onSwapTokenScreen {
+ warningTitle(title).assertVisibility(isDisplayed)
+ }
+ }
+ step("Assert warning icon is $assertDisplay") {
+ onSwapTokenScreen {
+ warningIcon(message).assertVisibility(isDisplayed)
+ }
+ }
+ step("Assert warning message is $assertDisplay") {
+ onSwapTokenScreen {
+ warningMessage(message).assertVisibility(isDisplayed)
+ }
+ }
+
+ if (swapButtonIsDisabled)
+ step("Assert 'Swap' button is disabled") {
+ onSwapTokenScreen {
+ swapButton.assertIsNotEnabled()
+ }
+ }
+ else
+ step("Assert 'Swap' button is enabled") {
+ onSwapTokenScreen {
+ swapButton.assertIsEnabled()
+ }
+ }
+}
+
sealed class SwapEntryPoint {
object MainScreen : SwapEntryPoint()
object TokenDetails : SwapEntryPoint()
@@ -177,4 +304,9 @@ sealed class SwapEntryPoint {
object TokenActionsBottomSheet : SwapEntryPoint()
}
+enum class FeeType {
+ Market,
+ Fast
+}
+
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt
index e8e256017c..6da3d7a7f5 100644
--- a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt
@@ -3,7 +3,8 @@ 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.TokenElementsTestTags
+import com.tangem.core.ui.test.AppBarWithSearchTestTags
+import com.tangem.core.ui.test.BuyTokenScreenTestTags
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
@@ -21,11 +22,30 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
hasText(getResourceString(R.string.exchange_tokens_available_tokens_header))
}
- fun tokenWithTitle(tokenTitle: String): KNode = child {
- hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
- hasAnyDescendant(withText(tokenTitle))
- useUnmergedTree = true
+ val searchIcon: KNode = child {
+ hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON)
+ }
+
+ val searchTextField: KNode = child {
+ hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD)
+ }
+
+ val noTokensFoundText: KNode = child {
+ hasText(getResourceString(R.string.express_token_list_empty_search))
+ }
+
+ fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child {
+ hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
+ hasAnyDescendant(withText(tokenTitle))
+ if (!availableForSwap) {
+ hasAnyDescendant(
+ withText(
+ getResourceString(R.string.tokens_list_unavailable_to_swap_source_header)
+ )
+ )
}
+ useUnmergedTree = true
+ }
}
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt
index 6705194521..007cfe289c 100644
--- a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt
@@ -2,6 +2,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
+import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
@@ -33,6 +34,12 @@ class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNode
hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT)
useUnmergedTree = true
}
+
+ val applyButton: KNode = child {
+ hasTestTag(BaseButtonTestTags.TEXT)
+ hasText(getResourceString(R.string.common_apply))
+ useUnmergedTree = true
+ }
}
internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
index 15be0dee77..30fe6823bd 100644
--- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
@@ -43,6 +43,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
+ val feeAmount: KNode = child {
+ hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT))
+ useUnmergedTree = true
+ }
+
val receiveAmountShimmer: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
}
@@ -71,6 +76,61 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
+ fun unableToCoverFeeNotificationTitle(networkName: String): KNode = child {
+ hasTestTag(NotificationTestTags.TITLE)
+ hasText(
+ getResourceString(
+ R.string.warning_express_not_enough_fee_for_token_tx_title,
+ networkName
+ )
+ )
+ useUnmergedTree = true
+ }
+
+ fun unableToCoverFeeNotificationText(currencyName: String, currencySymbol: String): KNode = child {
+ hasTestTag(NotificationTestTags.MESSAGE)
+ hasText(
+ getResourceString(
+ R.string.warning_express_not_enough_fee_for_token_tx_description,
+ currencyName,
+ currencySymbol
+ )
+ )
+ useUnmergedTree = true
+ }
+
+ fun unableToCoverFeeNotificationIcon(networkName: String): KNode = child {
+ hasTestTag(NotificationTestTags.ICON)
+ hasAnySibling(withTestTag(NotificationTestTags.TITLE))
+ hasAnySibling(
+ withText(
+ getResourceString(
+ R.string.warning_express_not_enough_fee_for_token_tx_title,
+ networkName,
+ )
+ )
+ )
+ useUnmergedTree = true
+ }
+
+ fun warningTitle(title: String): KNode = child {
+ hasTestTag(NotificationTestTags.TITLE)
+ hasText(title)
+ useUnmergedTree = true
+ }
+
+ fun warningMessage(message: String): KNode = child {
+ hasTestTag(NotificationTestTags.MESSAGE)
+ hasText(message)
+ useUnmergedTree = true
+ }
+
+ fun warningIcon(message: String): KNode = child {
+ hasTestTag(NotificationTestTags.ICON)
+ hasAnySibling(withText(message))
+ useUnmergedTree = true
+ }
+
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.warning_button_refresh))
@@ -95,15 +155,30 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
+ val insufficientFundsErrorTitle: KNode = child {
+ hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE)
+ hasText(getResourceString(R.string.swapping_insufficient_funds))
+ useUnmergedTree = true
+ }
+
val receiveFiatAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT)
}
+ val receiveFiatAmountWithPriceImpactWarning: KNode = child {
+ hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING)
+ }
+
+ val receiveFiatAmountInformationIcon: KNode = child {
+ hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON)
+ useUnmergedTree = true
+ }
+
val swapFiatAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT)
}
- val changeTokenIcon: KNode = child {
+ val selectTokenIcon: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON)
}
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt
index c084d381c8..54604ff5cf 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt
@@ -64,7 +64,7 @@ class DetailsTest : BaseTestCase() {
fun wallet2DetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
- openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
+ openMainScreen(productType = ProductType.Wallet2)
}
onTopBar {
step("Open wallet details") {
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt
index 0a20f32754..a1cc2319c9 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt
@@ -6,11 +6,12 @@ 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.clickWithAssertion
-import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.redux.StateDialog
-import com.tangem.scenarios.*
+import com.tangem.scenarios.checkFailedTransactionDialog
+import com.tangem.scenarios.checkScanWarningDialog
+import com.tangem.scenarios.openMainScreen
+import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
-import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
@@ -96,7 +97,7 @@ class FeedbackTest : BaseTestCase() {
step("Click 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
- step("Assert sanding text is displayed") {
+ step("Assert sеnding text is displayed") {
onSendConfirmScreen { sendingText.assertIsDisplayed() }
}
step("Click 'Send' button") {
@@ -163,39 +164,4 @@ class FeedbackTest : BaseTestCase() {
}
}
}
-
- @AllureId("3986")
- @DisplayName("Send feedback: from scan already used wallet alert dialog")
- @Test
- fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
- val gmailText = "Welcome to Gmail"
-
- setupHooks(
- additionalAfterSection = {
- device.uiDevice.pressBack()
- }
- ).run {
- step("Set mocks for Wallet2") {
- MockProvider.setMocks(ProductType.Wallet2)
- }
- step("Click on 'Accept' button") {
- onDisclaimerScreen { acceptButton.clickWithAssertion() }
- }
- step("Click on 'Get started' button") {
- onStoriesScreen { getStartedButton.clickWithAssertion() }
- }
- step("Click on 'Scan card or ring' button") {
- onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
- }
- step("Check 'Already used Wallet' dialog") {
- checkAlreadyUsedWalletDialog()
- }
- step("Click on 'Request support' button") {
- AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
- }
- step("Assert 'Gmail' app is open") {
- ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt
index c1e7d608de..26238b9789 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt
@@ -30,10 +30,7 @@ class OnboardingTest : BaseTestCase() {
@Test
fun shibaBackupScreenTest() {
setupHooks().run {
- scanCard(
- mockContent = ShibaNoBackupMockContent,
- alreadyActivatedDialogIsShown = true
- )
+ scanCard(mockContent = ShibaNoBackupMockContent)
checkBackupScreen()
}
}
@@ -54,10 +51,7 @@ class OnboardingTest : BaseTestCase() {
@Test
fun wallet2BackupScreenTest() {
setupHooks().run {
- scanCard(
- mockContent = Wallet2NoBackupMockContent,
- alreadyActivatedDialogIsShown = true
- )
+ scanCard(mockContent = Wallet2NoBackupMockContent)
checkBackupScreen()
}
}
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt
index 50a5f44b6d..60c2157e1a 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt
@@ -64,7 +64,7 @@ class ResetCardTest : BaseTestCase() {
fun resetWallet2CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
- openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
+ openMainScreen(productType = ProductType.Wallet2)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt
index 856a3963b3..1b83533fcb 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt
@@ -73,7 +73,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on $card") {
- openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
+ openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(
@@ -119,7 +119,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$card' card") {
- openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
+ openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)
@@ -138,7 +138,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$ring'") {
- openMainScreen(productType = cardType, alreadyActivatedDialogIsShown = true)
+ openMainScreen(productType = cardType)
}
step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)
@@ -175,7 +175,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$card' card") {
- openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
+ openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt
index 9bf7545be1..105eac0ecc 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt
@@ -1,9 +1,7 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
-import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
-import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
@@ -22,30 +20,32 @@ class SendTest : BaseTestCase() {
@DisplayName("Send: check fee notification")
@Test
fun checkFeeNotificationTest() {
- val currencyName = "POL (ex-MATIC)"
- val feeCurrencyName = "Ethereum"
- val feeCurrencySymbol = "ETH"
- val scenarioName = "eth_network_balance"
- val scenarioState = "Empty"
+ val currencyName = "USDC"
+ val feeCurrencyName = "Solana"
+ val feeCurrencySymbol = "SOL"
+ val balanceScenarioName = "solana_balance"
+ val tokensScenarioName = "user_tokens_api"
+ val balanceState = "Empty"
+ val tokensState = "SolanaUSDC"
setupHooks(
additionalAfterSection = {
- resetWireMockScenarioState(scenarioName)
+ resetWireMockScenarioState(balanceScenarioName)
+ resetWireMockScenarioState(tokensScenarioName)
}
).run {
- step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
- setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
+ step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") {
+ setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState)
+ }
+ step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") {
+ setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState)
}
-
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
- step("Swipe up") {
- swipeVertical(SwipeDirection.UP)
- }
step("Click on token with name: $currencyName") {
onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() }
}
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt
index cf0024f8bb..8bbaf0ed91 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt
@@ -75,7 +75,7 @@ class WarningTest : BaseTestCase() {
}
).run {
step("Open 'Main' screen") {
- openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true)
+ openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent)
}
step("Assert 'Seed phrase' notification icon is displayed") {
onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() }
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
index 5a158f6e07..f634fe21c0 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
@@ -2,17 +2,16 @@ package com.tangem.tests.actionButtons
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
-import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.assertIsDimmed
import com.tangem.common.extensions.clickWithAssertion
-import com.tangem.common.extensions.swipeVertical
-import com.tangem.common.utils.resetWireMockScenarioState
-import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
import com.tangem.scenarios.goToQrCodeBottomSheet
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
-import com.tangem.screens.*
+import com.tangem.screens.onMainScreen
+import com.tangem.screens.onSwapStoriesScreen
+import com.tangem.screens.onSwapTokenScreen
+import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@@ -124,97 +123,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
}
}
- @AllureId("4460")
- @DisplayName("Action buttons (token details screen): check 'Swap' button (provider error)")
- @Test
- fun checkSwapButtonProviderErrorTest() {
- val tokenTitle = "POL (ex-MATIC)"
-
- setupHooks().run {
- step("Open 'Main Screen'") {
- openMainScreen()
- }
- step("Synchronize addresses") {
- synchronizeAddresses()
- }
- step("Swipe up") {
- swipeVertical(SwipeDirection.UP)
- }
- step("Click on token with name: '$tokenTitle'") {
- waitForIdle()
- onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
- }
- step("Assert 'Swap' button is dimmed") {
- onTokenDetailsScreen { swapButton().assertIsDimmed() }
- }
- step("Click on 'Swap' button") {
- onTokenDetailsScreen { swapButton().performClick() }
- }
- step("Assert swapping $tokenTitle is not supported dialog text is displayed") {
- onSwapIsNotSupportedDialog { text(tokenTitle).assertIsDisplayed() }
- }
- step("Assert 'Ok' button is displayed") {
- onSwapIsNotSupportedDialog { okButton.assertIsDisplayed() }
- }
- step("Click on 'Ok' button") {
- onSwapIsNotSupportedDialog { okButton.performClick() }
- }
- step("Assert 'Swap' button is dimmed") {
- onTokenDetailsScreen { swapButton().assertIsDimmed() }
- }
- }
- }
-
- @AllureId("4461")
- @DisplayName("Action buttons (token details screen): check 'Swap' button (Express error)")
- @Test
- fun checkSwapButtonExpressErrorTest() {
- val tokenTitle = "Polygon"
- val scenarioName = "express_api_assets"
- 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("Swipe up") {
- swipeVertical(SwipeDirection.UP)
- }
- step("Click on token with name: '$tokenTitle'") {
- waitForIdle()
- onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
- }
- step("Assert 'Swap' button is dimmed") {
- onTokenDetailsScreen { swapButton().assertIsDimmed() }
- }
- step("Click on 'Swap' button") {
- onTokenDetailsScreen { swapButton().performClick() }
- }
- step("Assert operation is unavailable dialog text is displayed") {
- onOperationIsUnavailableDialog { text.assertIsDisplayed() }
- }
- step("Assert 'Ok' button is displayed") {
- onOperationIsUnavailableDialog { okButton.assertIsDisplayed() }
- }
- step("Click on 'Ok' button") {
- onOperationIsUnavailableDialog { okButton.performClick() }
- }
- step("Assert 'Swap' button is dimmed") {
- onTokenDetailsScreen { swapButton().assertIsDimmed() }
- }
- }
- }
-
@AllureId("3590")
@DisplayName("Action buttons (token details screen): validate UI")
@Test
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt
index c545a4100b..5105441296 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt
@@ -23,7 +23,7 @@ class KusamaWarningsTest : BaseTestCase() {
private val tokenName = "Kusama"
private val amountToLeaveLessThanDeposit = "0.300333"
private val amountToLeaveGreaterThanDeposit = "0.1"
- private val depositAmount = "KSM 0.000333333333"
+ private val depositAmount = "KSM 0.000003333"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt
index be40b64a78..31cf64adce 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt
@@ -23,7 +23,7 @@ class PolkadotWarningsTest : BaseTestCase() {
private val tokenName = "Polkadot"
private val amountToLeaveLessThanDeposit = "1.299"
private val amountToLeaveGreaterThanDeposit = "0.2"
- private val depositAmount = "DOT 1.00"
+ private val depositAmount = "DOT 0.01"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt
new file mode 100644
index 0000000000..f84fdea929
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt
@@ -0,0 +1,177 @@
+package com.tangem.tests.swap
+
+import com.tangem.common.BaseTestCase
+import com.tangem.common.annotations.ApiEnv
+import com.tangem.common.annotations.ApiEnvConfig
+import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
+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.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.qameta.allure.kotlin.AllureId
+import io.qameta.allure.kotlin.junit4.DisplayName
+import org.junit.Test
+
+@HiltAndroidTest
+class SwapChooseTokenScreenTest : BaseTestCase() {
+
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
+ @AllureId("8505")
+ @DisplayName("Swap: check available to swap tokens list")
+ @Test
+ fun checkAvailableToSwapTokensListTest() {
+ val tokenTitle = "Polygon"
+ val inputAmount = "100"
+ val ethereum = "Ethereum"
+ val polExMatic = "POL (ex-MATIC)"
+ val bitcoin = "Bitcoin"
+ val scenarioState = "CustomTokenAndJesusAdded"
+ val jesusCoin = "Jesus Coin"
+ val salam = "Salam"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Click on 'Select token' icon") {
+ onSwapTokenScreen { selectTokenIcon.performClick() }
+ }
+ step("Assert '$ethereum' is displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() }
+ }
+ step("Assert '$polExMatic' is displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
+ }
+ step("Assert '$bitcoin' is not displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() }
+ }
+ step("Assert '$jesusCoin' is displayed and unavailable for swap") {
+ onSwapChooseTokenScreen { tokenWithTitle(tokenTitle = jesusCoin).assertIsDisplayed() }
+ }
+ step("Assert custom token without backend id '$salam' is not displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() }
+ }
+ }
+ }
+
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
+ @AllureId("8506")
+ @DisplayName("Swap: check search on choose swap token screen")
+ @Test
+ fun checkSearchOnSwapChooseTokenScreenTest() {
+ val tokenTitle = "Polygon"
+ val inputAmount = "100"
+ val ethereum = "Ethereum"
+ val polExMatic = "POL (ex-MATIC)"
+ val polExMaticSymbol = "POL"
+ val invalidSearchText = "f"
+ val validSearchText = "pol"
+
+ setupHooks().run {
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Click on 'Select token' icon") {
+ onSwapTokenScreen { selectTokenIcon.performClick() }
+ }
+ step("Click on 'Search' icon") {
+ onSwapChooseTokenScreen { searchIcon.performClick() }
+ }
+ step("Click on 'Search' text field") {
+ onSwapChooseTokenScreen { searchTextField.performClick() }
+ }
+ step("Type invalid search text: '$invalidSearchText' in 'Search' text field") {
+ onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) }
+ }
+ step("Assert '$ethereum' is not displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
+ }
+ step("Assert '$polExMatic' is not displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() }
+ }
+ step("Press 'Delete' button") {
+ device.uiDevice.pressDelete()
+ }
+ step("Type valid search text: '$validSearchText' in 'Search' text field") {
+ onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) }
+ }
+ step("Assert '$ethereum' is not displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
+ }
+ step("Assert '$polExMatic' is displayed") {
+ onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
+ }
+ step("Select new receive token: $polExMatic") {
+ onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() }
+ }
+ step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") {
+ onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).assertIsDisplayed() }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt
index 2048e0fa7e..095b1ccacb 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt
@@ -4,16 +4,16 @@ import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
+import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.*
+import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.resetWireMockScenarios
+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.SwapEntryPoint
-import com.tangem.scenarios.openMainScreen
-import com.tangem.scenarios.openSwapScreen
-import com.tangem.scenarios.synchronizeAddresses
+import com.tangem.scenarios.*
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
@@ -266,6 +266,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
@AllureId("2828")
@DisplayName("Swap: network fee")
@Test
@@ -317,6 +320,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
@AllureId("575")
@DisplayName("Swap: check UI")
@Test
@@ -354,7 +360,7 @@ class SwapTokenScreenTest : BaseTestCase() {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
step("Click on 'Select token' icon") {
- onSwapTokenScreen { changeTokenIcon.performClick() }
+ onSwapTokenScreen { selectTokenIcon.performClick() }
}
step("Select new receive token: $newReceiveToken") {
onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() }
@@ -401,6 +407,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
@AllureId("5162")
@DisplayName("Swap: check swap tokens switch")
@Test
@@ -444,4 +453,262 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
}
+
+ @AllureId("573")
+ @DisplayName("Swap: check 'Swap' button availability")
+ @Test
+ fun checkSwapButtonAvailabilityTest() {
+ val polygon = "Polygon"
+ val bitcoin = "Bitcoin"
+ val salam = "Salam"
+ val scenarioState = "CustomTokenAndJesusAdded"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$polygon'") {
+ onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
+ }
+ step("Assert 'Swap' button is not dimmed. Swap available") {
+ onTokenDetailsScreen { swapButton().assertIsDimmed(false) }
+ }
+ step("Press 'Back' button") {
+ device.uiDevice.pressBack()
+ }
+ step("Click on token with name: '$bitcoin'. Swap unavailable") {
+ onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
+ }
+ step("Assert 'Swap' button is dimmed") {
+ onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
+ }
+ step("Press 'Back' button") {
+ device.uiDevice.pressBack()
+ }
+ step("Click on unknown custom token with name: '$salam'. Swap unavailable") {
+ onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
+ }
+ step("Assert 'Swap' button is dimmed") {
+ onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
+ }
+ }
+ }
+
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
+ @AllureId("583")
+ @DisplayName("Swap: check switch fee type (enable to cover 'Market' and 'Fast' fee)")
+ @Test
+ fun enableToCoverMarketAndFastFeeTest() {
+ val tokenName = "Ethereum"
+ val inputAmount = "0.99"
+ val market = "Market"
+ val fast = "Fast"
+ val marketFeeAmount = "$1.12"
+ val fastFeeAmount = "$1.43"
+
+ setupHooks().run {
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenName'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Select '$market' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount)
+ }
+ }
+ step("Select '$fast' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount)
+ }
+ }
+ }
+ }
+
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
+ @AllureId("8536")
+ @DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
+ @Test
+ fun unableToCoverMarketAndFastFeeTest() {
+ val tokenName = "POL (ex-MATIC)"
+ val inputAmount = "0.0001"
+ val marketFeeType = "Market"
+ val fastFeeType = "Fast"
+ val feeAmount = "$"
+ val scenarioName = "eth_network_balance"
+ val scenarioState = "LessThanDollar"
+ val networkName = "Ethereum"
+ val currencySymbol = "ETH"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(scenarioName)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
+ setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenName'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Select '$marketFeeType' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeType(feeType = FeeType.Market, feeAmount)
+ }
+ }
+ step("Check 'Unable to cover '$networkName' fee notification") {
+ chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
+ }
+ step("Assert 'Swap' button is disabled") {
+ onSwapTokenScreen { swapButton.assertIsNotEnabled() }
+ }
+ step("Select '$fastFeeType' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeType(feeType = FeeType.Fast, feeAmount)
+ }
+ }
+ step("Check 'Unable to cover '$networkName' fee notification") {
+ chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
+ }
+ step("Assert 'Swap' button is disabled") {
+ onSwapTokenScreen { swapButton.assertIsNotEnabled() }
+ }
+ }
+ }
+
+ @AllureId("8537")
+ @DisplayName("Swap: check switch fee type (unable to cover 'Fast' fee)")
+ @Test
+ fun unableToCoverFastFeeTest() {
+ val tokenName = "POL (ex-MATIC)"
+ val inputAmount = "3000"
+ val fastFeeType = "Fast"
+ val fastFeeAmount = "$2,"
+ val marketFeeType = "Market"
+ val marketFeeAmount = "$1."
+ val scenarioName = "eth_fee_history"
+ val scenarioState = "UnableToCoverFastFee"
+ val networkName = "Ethereum"
+ val currencySymbol = "ETH"
+
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(scenarioName)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
+ setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenName'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Swap' button is enabled") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSwapTokenScreen { swapButton.assertIsEnabled() }
+ }
+ }
+ step("Select '$fastFeeType' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeTypeWithGasless(feeType = FeeType.Fast, fastFeeAmount)
+ }
+ }
+ step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") {
+ onSwapTokenScreen { feeAmount.assertTextContains(fastFeeAmount, substring = true) }
+ }
+ step("Check 'Unable to cover '$networkName' fee notification") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
+ }
+ }
+ step("Assert 'Swap' button is disabled") {
+ onSwapTokenScreen { swapButton.assertIsNotEnabled() }
+ }
+ step("Select '$marketFeeType' fee type") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ selectFeeTypeWithGasless(feeType = FeeType.Market, marketFeeAmount)
+ }
+ }
+ step("Assert 'Swap' button is enabled") {
+ waitForIdle()
+ onSwapTokenScreen { swapButton.assertIsEnabled() }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt
new file mode 100644
index 0000000000..ba385763a9
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt
@@ -0,0 +1,580 @@
+package com.tangem.tests.swap
+
+import com.tangem.common.BaseTestCase
+import com.tangem.common.R
+import com.tangem.common.annotations.ApiEnv
+import com.tangem.common.annotations.ApiEnvConfig
+import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
+import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
+import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
+import com.tangem.common.extensions.*
+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.SwapEntryPoint
+import com.tangem.scenarios.chackUnableToCoverFeeNotification
+import com.tangem.scenarios.checkSwapWarning
+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 SwapTokenScreenWarningsTest : BaseTestCase() {
+
+ @ApiEnv(
+ ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
+ )
+ @AllureId("580")
+ @DisplayName("Swap: check 'Insufficient funds' warning")
+ @Test
+ fun checkSwapInsufficientFundsWarningTest() {
+ val tokenTitle = "Polygon"
+ val inputAmount = "1000"
+
+ setupHooks().run {
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Insufficient funds' error is displayed") {
+ waitForIdle()
+ onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() }
+ }
+ }
+ }
+
+ @AllureId("8502")
+ @DisplayName("Swap: check 'Unable to cover network fee' warning")
+ @Test
+ fun checkUnableToCoverBlockchainFeeWarningTest() {
+ val tokenTitle = "USDC"
+ val inputAmount = "1000"
+ val tokensScenarioState = "SolanaUSDC"
+ val balanceScenarioName = "solana_balance"
+ val balanceScenarioState = "Empty"
+ val networkName = "Solana"
+ val currencySymbol = "SOL"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ resetWireMockScenarioState(balanceScenarioName)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") {
+ setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Check 'Unable to cover '$networkName' fee notification") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
+ }
+ }
+ step("Assert 'Swap' button is disabled") {
+ onSwapTokenScreen { swapButton.assertIsNotEnabled() }
+ }
+ }
+ }
+
+ @AllureId("8503")
+ @DisplayName("Swap: check 'High price impact' warning on CEX")
+ @Test
+ fun checkHighPriceImpactWarningCEXTest() {
+ val tokenTitle = "USDC"
+ val inputAmount = "100"
+ val currencySymbol = "SOL"
+ val slippagePercent = "5%"
+ val tokensScenarioState = "SolanaUSDC"
+ val exchangeQuoteScenarioName = "exchange_quote_solana"
+ val exchangeQuoteScenarioState = "HighPriceImpact"
+ val dialogTitle = getResourceString(R.string.swapping_alert_title)
+ val dialogText = getResourceString(
+ R.string.swapping_alert_cex_description_with_slippage,
+ currencySymbol,
+ slippagePercent
+ )
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ resetWireMockScenarioState(exchangeQuoteScenarioName)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$exchangeQuoteScenarioName' to state: $exchangeQuoteScenarioState") {
+ setWireMockScenarioState(
+ scenarioName = exchangeQuoteScenarioName,
+ state = exchangeQuoteScenarioState
+ )
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert fiat amount with warning is displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSwapTokenScreen {
+ waitForIdle()
+ receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true)
+ }
+ }
+ }
+ step("Assert receive amount information icon is displayed") {
+ onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() }
+ }
+ step("Click on receive amount information icon") {
+ onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() }
+ }
+ step("Assert information dialog is displayed") {
+ onDialog { dialogContainer.assertIsDisplayed() }
+ }
+ step("Assert information dialog title is displayed") {
+ onDialog { title.assertTextEquals(dialogTitle) }
+ }
+ step("Assert information dialog text for CEX is displayed") {
+ onDialog { text.assertTextEquals(dialogText) }
+ }
+ step("Assert dialog 'OK' button is displayed") {
+ onDialog { okButton.assertIsDisplayed() }
+ }
+ }
+ }
+
+ @AllureId("8504")
+ @DisplayName("Swap: check 'High price impact' warning on DEX")
+ @Test
+ fun checkHighPriceImpactWarningDEXTest() {
+ val tokenTitle = "Polygon"
+ val inputAmount = "1000"
+ val slippagePercent = "3.5%"
+ val dialogTitle = getResourceString(R.string.swapping_alert_title)
+ val highPriceImpactDescription = getResourceString(R.string.swapping_high_price_impact_description)
+ val swappingAlertDEXDescription = getResourceString(R.string.swapping_alert_dex_description)
+ val swappingAlertDEXDescriptionWithSlippage = getResourceString(
+ R.string.swapping_alert_dex_description_with_slippage,
+ slippagePercent
+ )
+ val pairsToScenarioName = "polygon_pos_to_pairs"
+ val pairsFromScenarioName = "polygon_pos_from_pairs"
+ val scenarioState = "DexProvider"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(pairsToScenarioName)
+ resetWireMockScenarioState(pairsFromScenarioName)
+ }
+ ).run {
+ step("Set WireMock scenario: '$pairsToScenarioName' to state: $scenarioState") {
+ setWireMockScenarioState(scenarioName = pairsToScenarioName, state = scenarioState)
+ }
+ step("Set WireMock scenario: '$pairsFromScenarioName' to state: $scenarioState") {
+ setWireMockScenarioState(scenarioName = pairsFromScenarioName, state = scenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert fiat amount with warning is displayed") {
+ onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) }
+ }
+ step("Assert receive amount information icon is displayed") {
+ onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() }
+ }
+ step("Click on receive amount information icon") {
+ onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() }
+ }
+ step("Assert information dialog is displayed") {
+ onDialog { dialogContainer.assertIsDisplayed() }
+ }
+ step("Assert information dialog title is displayed") {
+ onDialog { title.assertTextEquals(dialogTitle) }
+ }
+ step("Assert information dialog text for DEX is displayed") {
+ onDialog {
+ text.assertTextContains(highPriceImpactDescription, substring = true)
+ text.assertTextContains(swappingAlertDEXDescription, substring = true)
+ text.assertTextContains(swappingAlertDEXDescriptionWithSlippage, substring = true)
+ }
+ }
+ step("Assert dialog 'OK' button is displayed") {
+ onDialog { okButton.assertIsDisplayed() }
+ }
+ step("Click on 'OK' button") {
+ onDialog { okButton.performClick() }
+ }
+ }
+ }
+
+ @AllureId("2831")
+ @DisplayName("Swap: warning is not displayed, if remaining balance is equal to 0")
+ @Test
+ fun solanaRemainingBalanceEqualToZeroWarningTest() {
+ val tokenTitle = "Solana"
+ val inputAmount = "0.00168933"
+ val tokensScenarioState = "SolanaUSDC"
+ val rentAmount = "SOL 0.00089088"
+ val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title)
+ val notificationMessage = getResourceString(
+ R.string.send_notification_invalid_amount_rent_fee,
+ rentAmount
+ )
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Invalid amount' warning is not displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ checkSwapWarning(
+ title = notificationTitle,
+ message = notificationMessage,
+ isDisplayed = false
+ )
+ }
+ }
+ }
+ }
+
+ @AllureId("2832")
+ @DisplayName("Swap: warning is not displayed, if remaining balance is equal to rent amount")
+ @Test
+ fun solanaRemainingBalanceEqualToRentAmountTest() {
+ val tokenTitle = "Solana"
+ val inputAmount = "0.001689338"
+ val tokensScenarioState = "SolanaUSDC"
+ val rentAmount = "SOL 0.00089088"
+ val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title)
+ val notificationMessage = getResourceString(
+ R.string.send_notification_invalid_amount_rent_fee,
+ rentAmount
+ )
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Invalid amount' warning is not displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ checkSwapWarning(
+ title = notificationTitle,
+ message = notificationMessage,
+ isDisplayed = false
+ )
+ }
+
+ }
+ }
+ }
+
+ @AllureId("2833")
+ @DisplayName("Swap: warning is not displayed, if remaining balance more than rent amount")
+ @Test
+ fun solanaRemainingBalanceMoreThanRentAmountTest() {
+ val tokenTitle = "Solana"
+ val inputAmount = "0.0000941"
+ val tokensScenarioState = "SolanaUSDC"
+ val rentAmount = "SOL 0.00089088"
+ val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title)
+ val notificationMessage = getResourceString(
+ R.string.send_notification_invalid_amount_rent_fee,
+ rentAmount
+ )
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Invalid amount' warning is not displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ checkSwapWarning(
+ title = notificationTitle,
+ message = notificationMessage,
+ isDisplayed = false
+ )
+ }
+ }
+ }
+ }
+
+ @AllureId("2830")
+ @DisplayName("Swap: warning is displayed, if remaining balance less than rent amount")
+ @Test
+ fun solanaRemainingBalanceLessThanRentAmountTest() {
+ val tokenTitle = "Solana"
+ val inputAmount = "0.0016941"
+ val tokensScenarioState = "SolanaUSDC"
+ val rentAmount = "SOL 0.00089088"
+ val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title)
+ val notificationMessage = getResourceString(
+ R.string.send_notification_invalid_amount_rent_fee,
+ rentAmount
+ )
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ }
+ ).run {
+
+ step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
+ }
+ step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
+ }
+
+ step("Open 'Main Screen'") {
+ openMainScreen()
+ }
+ step("Synchronize addresses") {
+ synchronizeAddresses()
+ }
+ step("Click on token with name: '$tokenTitle'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
+ }
+ step("Assert title: '$tokenTitle' is displayed") {
+ onTokenDetailsScreen { title.assertIsDisplayed() }
+ }
+ step("Open 'Swap' screen") {
+ openSwapScreen(from = SwapEntryPoint.TokenDetails)
+ }
+ step("Assert 'You swap' block is displayed") {
+ onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
+ }
+ step("Input swap amount = '$inputAmount'") {
+ waitForIdle()
+ onSwapTokenScreen {
+ textInput.clickWithAssertion()
+ textInput.performTextReplacement(inputAmount)
+ }
+ }
+ step("Assert 'Invalid amount' warning is displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ checkSwapWarning(
+ title = notificationTitle,
+ message = notificationMessage
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..9878c6fe3f
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/internal/res/drawable/ic_launcher_foreground.xml b/app/src/internal/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..4a9d1d1208
--- /dev/null
+++ b/app/src/internal/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config
index 827750b5a0..636d7a8aa0 160000
--- a/app/src/main/assets/tangem-app-config
+++ b/app/src/main/assets/tangem-app-config
@@ -1 +1 @@
-Subproject commit 827750b5a0ef7202120b0df0ae903d70e18e3d32
+Subproject commit 636d7a8aa0e330e9b95e91d85f23ad15ac6d913f
diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt
index 97f825c6ba..54e4f52423 100644
--- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt
+++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt
@@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+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
@@ -43,12 +43,12 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
+import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@@ -58,12 +58,12 @@ import dagger.hilt.components.SingletonComponent
@Suppress("TooManyFunctions")
interface ApplicationEntryPoint {
- fun getEnvironmentConfigStorage(): EnvironmentConfigStorage
-
fun getAppStateHolder(): AppStateHolder
fun getIssuersConfigStorage(): IssuersConfigStorage
+ fun getEnvironmentConfig(): EnvironmentConfig
+
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
@@ -120,8 +120,6 @@ interface ApplicationEntryPoint {
fun getOnboardingRepository(): OnboardingRepository
- fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
-
fun getExcludedBlockchains(): ExcludedBlockchains
fun getAppLogsStore(): AppLogsStore
@@ -154,4 +152,6 @@ interface ApplicationEntryPoint {
fun getABTestsManager(): ABTestsManager
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
+
+ fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
index 809f2808bc..3124e26244 100644
--- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
+++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
@@ -9,6 +9,7 @@ import androidx.work.WorkManager
import com.tangem.common.routing.AppRoute
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.repositories.SettingsRepository
+import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
@@ -28,6 +29,7 @@ internal class LockUserWalletsTimer(
private val userWalletsListRepository: UserWalletsListRepository,
private val coroutineScope: CoroutineScope,
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
+ private val passwordRequester: HotWalletPasswordRequester,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@@ -53,6 +55,7 @@ internal class LockUserWalletsTimer(
)
if (shouldOpenWelcomeScreenOnResume) {
+ passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
@@ -116,6 +119,7 @@ internal class LockUserWalletsTimer(
start()
}
.onRight {
+ passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index cf540d0ec0..d6c1a7abf3 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@@ -29,6 +30,8 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.ui.extensions.LocalUserInteractionTracker
+import com.tangem.core.ui.extensions.UserInteractionTracker
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
@@ -43,8 +46,8 @@ import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
+import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
-import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
@@ -162,7 +165,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
@Inject
- internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
+ internal lateinit var passwordRequester: HotWalletPasswordRequester
private val viewModel: MainViewModel by viewModels()
@@ -235,8 +238,16 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intent),
)
+ val userInteractionTracker = object : UserInteractionTracker {
+ override fun onUserInteraction() {
+ lockUserWalletsTimer?.restart()
+ }
+ }
+
setContent {
- routingComponent.Content(Modifier.fillMaxSize())
+ CompositionLocalProvider(LocalUserInteractionTracker provides userInteractionTracker) {
+ routingComponent.Content(Modifier.fillMaxSize())
+ }
}
}
@@ -253,6 +264,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
+ passwordRequester = passwordRequester,
)
store.dispatch(
diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt
index a22cc0dd22..3fe333e207 100644
--- a/app/src/main/java/com/tangem/tap/TangemApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt
@@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@@ -62,34 +61,32 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.attestation.api.TangemApiServiceSettings
import com.tangem.tap.common.analytics.AnalyticsFactory
+import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
+import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
-import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import timber.log.Timber
lateinit var store: Store
val foregroundActivityObserver = ForegroundActivityObserver
-internal lateinit var derivationsFinder: DerivationsFinder
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
@@ -100,12 +97,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val appStateHolder: AppStateHolder
get() = entryPoint.getAppStateHolder()
- private val environmentConfigStorage: EnvironmentConfigStorage
- get() = entryPoint.getEnvironmentConfigStorage()
-
private val issuersConfigStorage: IssuersConfigStorage
get() = entryPoint.getIssuersConfigStorage()
+ private val environmentConfig: EnvironmentConfig
+ get() = entryPoint.getEnvironmentConfig()
+
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
@@ -190,9 +187,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val onboardingRepository: OnboardingRepository
get() = entryPoint.getOnboardingRepository()
- private val dispatchers: CoroutineDispatcherProvider
- get() = entryPoint.getCoroutineDispatcherProvider()
-
private val excludedBlockchains: ExcludedBlockchains
get() = entryPoint.getExcludedBlockchains()
@@ -246,6 +240,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val appsFlyerClientFactory: AppsFlyerClient.Factory
get() = entryPoint.getAppsFlyerClientFactory()
+ private val customerIoFeatureToggles: CustomerIoFeatureToggles
+ get() = entryPoint.getCustomerIoFeatureToggles()
+
// endregion
private val appScope = MainScope()
@@ -312,9 +309,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
Timber.i(excludedBlockchainsManager.toString())
}
- runBlocking {
- initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
- }
+ initWithConfigDependency(environmentConfig = environmentConfig)
abTestsManager.init()
@@ -348,15 +343,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
)
}
- derivationsFinder = DerivationsFinder(
- userTokensResponseStore = userTokensResponseStore,
- dispatchers = dispatchers,
- )
-
appStateHolder.mainStore = store
wcInitializeUseCase.init(
- projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
+ projectId = environmentConfig.walletConnectProjectId,
)
}
@@ -387,7 +377,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
shareManager = shareManager,
appRouter = appRouter,
transactionSignerFactory = transactionSignerFactory,
- environmentConfigStorage = environmentConfigStorage,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
onboardingRepository = onboardingRepository,
excludedBlockchains = excludedBlockchains,
@@ -427,6 +416,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
+ if (customerIoFeatureToggles.isFeatureEnabled) {
+ factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder())
+ }
+
factory.addFilter(oneTimeEventFilter)
factory.addFilter(AppsFlyerEventFilter())
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt
new file mode 100644
index 0000000000..ae70817e3a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt
@@ -0,0 +1,12 @@
+package com.tangem.tap.common.analytics
+
+import com.tangem.core.configtoggle.feature.FeatureTogglesManager
+import javax.inject.Inject
+
+class CustomerIoFeatureToggles @Inject constructor(
+ private val featureTogglesManager: FeatureTogglesManager,
+) {
+
+ val isFeatureEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled(name = "CUSTOMER_IO_ENABLED")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt
new file mode 100644
index 0000000000..5c81ccfd73
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap.common.analytics.handlers.customerio
+
+import com.tangem.core.analytics.api.UserIdHolder
+
+/**
+ * Client interface for Customer.io SDK operations.
+ */
+interface CustomerIoAnalyticsClient : UserIdHolder
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt
new file mode 100644
index 0000000000..a86b651af3
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt
@@ -0,0 +1,51 @@
+package com.tangem.tap.common.analytics.handlers.customerio
+
+import com.tangem.core.analytics.api.AnalyticsHandler
+import com.tangem.core.analytics.api.AnalyticsUserIdHandler
+import com.tangem.core.analytics.models.AnalyticsEvent
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
+
+/**
+ * Customer.io analytics handler.
+ */
+class CustomerIoAnalyticsHandler(
+ private val client: CustomerIoAnalyticsClient,
+) : AnalyticsHandler, AnalyticsUserIdHandler {
+
+ override fun id(): String = ID
+
+ override fun setUserId(userId: String) {
+ client.setUserId(userId)
+ }
+
+ override fun clearUserId() {
+ client.clearUserId()
+ }
+
+ override fun send(event: AnalyticsEvent) {
+ // No-op: product events are not sent to Customer.io.
+ // Triggers are configured to come from Amplitude directly.
+ }
+
+ companion object {
+ const val ID = "CustomerIO"
+ }
+
+ class Builder : AnalyticsHandlerBuilder {
+ override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
+ val cdpApiKey = data.config.customerIoCdpApiKey
+ return if (data.logConfig.isCustomerIoLogEnabled) {
+ CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
+ } else if (!cdpApiKey.isNullOrBlank()) {
+ CustomerIoAnalyticsHandler(
+ client = CustomerIoClient(
+ application = data.application,
+ cdpApiKey = cdpApiKey,
+ ),
+ )
+ } else {
+ null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt
new file mode 100644
index 0000000000..ddd534e938
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt
@@ -0,0 +1,45 @@
+package com.tangem.tap.common.analytics.handlers.customerio
+
+import android.app.Application
+import io.customer.messagingpush.ModuleMessagingPushFCM
+import io.customer.sdk.CustomerIO
+import io.customer.sdk.CustomerIOBuilder
+import io.customer.sdk.data.model.Region
+import timber.log.Timber
+
+/**
+ * Real Customer.io SDK client.
+ *
+ * Initializes the SDK with the given CDP API key and provides:
+ * - User identification (identify / clearIdentify)
+ *
+ * Auto-tracking of application lifecycle events is disabled since it is not needed.
+ * Auto-tracking of screen views is disabled.
+ */
+internal class CustomerIoClient(
+ application: Application,
+ cdpApiKey: String,
+) : CustomerIoAnalyticsClient {
+
+ init {
+ CustomerIOBuilder(
+ applicationContext = application,
+ cdpApiKey = cdpApiKey,
+ )
+ .region(Region.EU)
+ .trackApplicationLifecycleEvents(false)
+ .autoTrackActivityScreens(false)
+ .addCustomerIOModule(ModuleMessagingPushFCM())
+ .build()
+
+ Timber.d("CustomerIO SDK initialized")
+ }
+
+ override fun setUserId(userId: String) {
+ CustomerIO.instance().identify(userId = userId)
+ }
+
+ override fun clearUserId() {
+ CustomerIO.instance().clearIdentify()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt
new file mode 100644
index 0000000000..62a36d09f1
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt
@@ -0,0 +1,23 @@
+package com.tangem.tap.common.analytics.handlers.customerio
+
+import timber.log.Timber
+
+/**
+ * Log client for Customer.io (used in debug mode).
+ *
+ * Logs all operations to Timber instead of sending them to Customer.io.
+ */
+internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
+
+ private var userId: String? = null
+
+ override fun setUserId(userId: String) {
+ this.userId = userId
+ Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
+ }
+
+ override fun clearUserId() {
+ Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
+ this.userId = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt
index 97026725ea..9965c9a2f2 100644
--- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt
+++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt
@@ -3,11 +3,19 @@ package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
+import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
+import dagger.hilt.android.AndroidEntryPoint
+import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import timber.log.Timber
+import javax.inject.Inject
+@AndroidEntryPoint
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
+ @Inject
+ lateinit var customerIoFeatureToggles: CustomerIoFeatureToggles
+
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
@@ -15,11 +23,23 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
+
+ if (customerIoFeatureToggles.isFeatureEnabled) {
+ CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)
+ }
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
+ if (customerIoFeatureToggles.isFeatureEnabled) {
+ CustomerIOFirebaseMessagingService.onMessageReceived(
+ context = applicationContext,
+ remoteMessage = message,
+ handleNotificationTrigger = false,
+ )
+ }
+
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
index b43504d06c..ae347d0807 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
@@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
-import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@@ -29,7 +28,6 @@ data class AppState(
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
- TradeCryptoMiddleware.middleware,
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt
new file mode 100644
index 0000000000..89c89aef25
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt
@@ -0,0 +1,29 @@
+package com.tangem.tap.data
+
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.offramp.repository.OfframpRepository
+import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
+import com.tangem.tap.network.exchangeServices.SellService
+
+/**
+ * Default implementation of [OfframpRepository]
+ *
+ * @property sellService sell service for getting offramp URL
+ */
+internal class DefaultOfframpRepository(
+ private val sellService: SellService,
+) : OfframpRepository {
+
+ override fun getOfframpUrl(
+ cryptoCurrency: CryptoCurrency,
+ fiatCurrencyCode: String,
+ walletAddress: String,
+ ): String? {
+ return sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt
index 0d9a4eba92..595d7ccdcb 100644
--- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt
+++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt
@@ -62,23 +62,19 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore
- .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
- }
+ appPreferencesStore
+ .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
}
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
- return withContext(dispatcherProvider.io) {
- appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId))
- .takeIf { !it.isNullOrEmpty() }
- }
+ return appPreferencesStore.getSyncOrNull(
+ key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
+ )
+ .takeIf { !it.isNullOrEmpty() }
}
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
- }
+ appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
}
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
@@ -92,58 +88,57 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
- val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
- ?.decodeToString(throwOnInvalidSequence = true)
- ?.let(tokensAdapter::fromJson)
+ return withContext(dispatcherProvider.io) {
+ val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
+ ?.decodeToString(throwOnInvalidSequence = true)
+ ?.let(tokensAdapter::fromJson)
- return authTokens?.let { tokens ->
- if (tokens.idempotencyKey == null) {
- val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
- storeAuthTokens(customerWalletAddress, newAuthTokens)
- newAuthTokens
- } else {
- tokens
+ authTokens?.let { tokens ->
+ if (tokens.idempotencyKey == null) {
+ val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
+ storeAuthTokens(customerWalletAddress, newAuthTokens)
+ newAuthTokens
+ } else {
+ tokens
+ }
}
}
}
override suspend fun clearAuthTokens(customerWalletAddress: String) {
- secureStorage.delete(createAuthTokensKey(customerWalletAddress))
+ withContext(dispatcherProvider.io) {
+ secureStorage.delete(createAuthTokensKey(customerWalletAddress))
+ }
}
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
- }
+ appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
}
override suspend fun getOrderId(customerWalletAddress: String): String? {
- return withContext(dispatcherProvider.io) {
- appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
- .takeIf { !it.isNullOrEmpty() }
- }
+ return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
+ .takeIf { !it.isNullOrEmpty() }
}
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
- return withContext(dispatcherProvider.io) {
- appPreferencesStore.getSyncOrNull(
- key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
- ) == true
- }
+ return appPreferencesStore.getSyncOrNull(
+ key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
+ ) == true
}
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
- }
+ appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
}
- override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
+ override suspend fun clearOrderId(customerWalletAddress: String) {
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
}
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
- appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), isPaeraCustomer)
+ appPreferencesStore.store(
+ PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
+ isPaeraCustomer,
+ )
}
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
@@ -152,7 +147,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
- val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
+ val orders = mutablePreferences.getObjectMap(
+ PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
+ )
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
@@ -179,7 +176,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
- val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
+ val orders = appPreferencesStore.getObjectMapSync(
+ PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
+ )
return orders[createWithdrawOrderIdKey(userWalletId)]
}
@@ -191,7 +190,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
- val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
+ val orders = mutablePreferences.getObjectMap(
+ PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
+ )
.minus(createWithdrawOrderIdKey(userWalletId))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
@@ -221,40 +222,33 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
- }
+ appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
- return withContext(dispatcherProvider.io) {
- appPreferencesStore.getSyncOrNull(
- key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
- ) == true
- }
+ return appPreferencesStore.getSyncOrNull(
+ key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
+ ) == true
}
override suspend fun storeTangemPayEligibility(eligibility: Boolean) {
- withContext(dispatcherProvider.io) {
- appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
- }
+ appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
}
override suspend fun getTangemPayEligibility(): Boolean {
- return withContext(dispatcherProvider.io) {
- appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
- }
+ return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
}
- override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
+ override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
- appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
- appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
- appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
- appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
- appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
}
+ appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
+ appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
+ appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
+ appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
+ appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
+ }
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt
index 1f40496096..e5600cc208 100644
--- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.di
import com.tangem.datasource.api.moonpay.MoonPayApi
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
@@ -69,14 +69,14 @@ internal object ActivityModule {
@Provides
@Singleton
fun provideExchangeService(
- environmentConfigStorage: EnvironmentConfigStorage,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
moonPayApi: MoonPayApi,
+ environmentConfig: EnvironmentConfig,
): SellService {
return MoonPayService(
api = moonPayApi,
- apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
- secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
+ apiKey = environmentConfig.moonPayApiKey,
+ secretKey = environmentConfig.moonPayApiSecretKey,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt
index 38e66db21b..53f53dffef 100644
--- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt
@@ -1,6 +1,7 @@
package com.tangem.tap.di
import android.content.Context
+import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
@@ -12,6 +13,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
+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
@@ -40,6 +42,8 @@ internal class TangemSdkManagerModule {
appFinisher: AppFinisher,
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
analyticsExceptionHandler: AnalyticsExceptionHandler,
+ blockchainToDeriveFinder: BlockchainToDeriveFinder,
+ analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
@@ -56,6 +60,8 @@ internal class TangemSdkManagerModule {
appFinisher = appFinisher,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
analyticsExceptionHandler = analyticsExceptionHandler,
+ blockchainToDeriveFinder = blockchainToDeriveFinder,
+ analyticsEventHandler = analyticsEventHandler,
dispatchers = dispatchers,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt
index 569c69c6b8..1240259f77 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt
@@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@@ -97,12 +96,10 @@ internal object AccountDomainModule {
fun provideIsAccountsModeEnabledUseCase(
userWalletsListRepository: UserWalletsListRepository,
accountsCRUDRepository: AccountsCRUDRepository,
- accountsFeatureToggles: AccountsFeatureToggles,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
userWalletsListRepository = userWalletsListRepository,
crudRepository = accountsCRUDRepository,
- accountsFeatureToggles = accountsFeatureToggles,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt
index 3ae0f6d5c6..d6555cd643 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt
@@ -1,9 +1,9 @@
package com.tangem.tap.di.domain
+import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.earn.usecase.*
-import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -19,15 +19,15 @@ object EarnDomainModule {
}
@Provides
- fun provideManageEarnNetworksUseCase(
+ fun provideGetEarnNetworksUseCase(
earnRepository: EarnRepository,
+ multiAccountListSupplier: MultiAccountListSupplier,
userWalletsListRepository: UserWalletsListRepository,
- multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
): GetEarnNetworksUseCase {
return GetEarnNetworksUseCase(
earnRepository = earnRepository,
+ multiAccountListSupplier = multiAccountListSupplier,
userWalletsListRepository = userWalletsListRepository,
- multiNetworkStatusSupplier = multiNetworkStatusSupplier,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt
index b1d7b0a206..b527533fc1 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt
@@ -3,20 +3,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
-import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
-import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
-import com.tangem.domain.staking.StakingIdFactory
-import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
-import com.tangem.domain.tokens.repository.CurrenciesRepository
-import com.tangem.domain.walletmanager.WalletManagersFacade
-import com.tangem.domain.wallets.derivations.DerivationsRepository
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
@Module
@@ -57,40 +48,6 @@ internal object ManageTokensDomainModule {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
- @Provides
- @Singleton
- fun provideRemoveCustomManagedCryptoCurrencyUseCase(
- customTokensRepository: CustomTokensRepository,
- ): RemoveCustomManagedCryptoCurrencyUseCase {
- return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
- }
-
- @Provides
- @Singleton
- fun provideSaveManagedTokensUseCase(
- customTokensRepository: CustomTokensRepository,
- walletManagersFacade: WalletManagersFacade,
- currenciesRepository: CurrenciesRepository,
- derivationsRepository: DerivationsRepository,
- multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
- multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
- multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
- stakingIdFactory: StakingIdFactory,
- dispatchers: CoroutineDispatcherProvider,
- ): SaveManagedTokensUseCase {
- return SaveManagedTokensUseCase(
- customTokensRepository = customTokensRepository,
- walletManagersFacade = walletManagersFacade,
- currenciesRepository = currenciesRepository,
- derivationsRepository = derivationsRepository,
- multiNetworkStatusFetcher = multiNetworkStatusFetcher,
- multiQuoteStatusFetcher = multiQuoteStatusFetcher,
- multiStakingBalanceFetcher = multiStakingBalanceFetcher,
- stakingIdFactory = stakingIdFactory,
- parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
- )
- }
-
@Provides
@Singleton
fun provideGetSupportedNetworksUseCase(
diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
index d96861be6a..64d1c68c6c 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
@@ -4,22 +4,11 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
-import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
-import com.tangem.domain.promo.PromoRepository
-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.tokens.repository.CurrenciesRepository
-import com.tangem.domain.walletmanager.WalletManagersFacade
-import com.tangem.domain.wallets.derivations.DerivationsRepository
-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
@Module
@@ -66,32 +55,6 @@ object MarketsDomainModule {
return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier)
}
- @Provides
- @Singleton
- fun provideSaveMarketTokensUseCase(
- derivationsRepository: DerivationsRepository,
- marketsTokenRepository: MarketsTokenRepository,
- walletManagersFacade: WalletManagersFacade,
- currenciesRepository: CurrenciesRepository,
- multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
- multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
- multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
- stakingIdFactory: StakingIdFactory,
- dispatchers: CoroutineDispatcherProvider,
- ): SaveMarketTokensUseCase {
- return SaveMarketTokensUseCase(
- derivationsRepository = derivationsRepository,
- marketsTokenRepository = marketsTokenRepository,
- walletManagersFacade = walletManagersFacade,
- currenciesRepository = currenciesRepository,
- multiNetworkStatusFetcher = multiNetworkStatusFetcher,
- multiQuoteStatusFetcher = multiQuoteStatusFetcher,
- multiStakingBalanceFetcher = multiStakingBalanceFetcher,
- stakingIdFactory = stakingIdFactory,
- parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
- )
- }
-
@Provides
@Singleton
fun provideGetTokenMarketCryptoCurrency(
@@ -119,16 +82,4 @@ object MarketsDomainModule {
fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase {
return GetTokenExchangesUseCase(marketsTokenRepository = marketsTokenRepository)
}
-
- @Provides
- @Singleton
- fun provideShouldShowYieldModeMarketPromoUseCase(
- promoRepository: PromoRepository,
- marketsTokenRepository: MarketsTokenRepository,
- ): ShouldShowYieldModeMarketPromoUseCase {
- return ShouldShowYieldModeMarketPromoUseCase(
- promoRepository = promoRepository,
- marketsTokenRepository = marketsTokenRepository,
- )
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
index cf1a7707c6..4cbe877c01 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
@@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
@@ -25,15 +24,11 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesGetNFTCollectionsUseCase(
- currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountListSupplier: SingleAccountListSupplier,
- accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
- currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountListSupplier = singleAccountListSupplier,
- accountsFeatureToggles = accountsFeatureToggles,
)
@Provides
diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt
index 34ffd4aaba..ad1ba0be09 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt
@@ -1,9 +1,13 @@
package com.tangem.tap.di.domain
+import com.tangem.domain.offramp.GetOfframpUrlUseCase
+import com.tangem.domain.offramp.repository.OfframpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.repositories.SettingsRepository
+import com.tangem.tap.data.DefaultOfframpRepository
+import com.tangem.tap.network.exchangeServices.SellService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -275,4 +279,16 @@ internal object OnrampDomainModule {
promoRepository = promoRepository,
)
}
+
+ @Provides
+ @Singleton
+ fun provideOfframpRepository(sellService: SellService): OfframpRepository {
+ return DefaultOfframpRepository(sellService)
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
+ return GetOfframpUrlUseCase(offrampRepository)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
index 5bb200ad04..8e58e90e09 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
@@ -7,6 +7,7 @@ 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
@@ -19,7 +20,6 @@ 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.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
@@ -39,28 +39,6 @@ import javax.inject.Singleton
@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule {
- @Provides
- @Singleton
- fun provideAddCryptoCurrenciesUseCase(
- currenciesRepository: CurrenciesRepository,
- walletManagersFacade: WalletManagersFacade,
- multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
- multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
- singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
- multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
- stakingIdFactory: StakingIdFactory,
- ): AddCryptoCurrenciesUseCase {
- return AddCryptoCurrenciesUseCase(
- currenciesRepository = currenciesRepository,
- walletManagersFacade = walletManagersFacade,
- multiNetworkStatusFetcher = multiNetworkStatusFetcher,
- multiQuoteStatusFetcher = multiQuoteStatusFetcher,
- singleStakingBalanceFetcher = singleStakingBalanceFetcher,
- multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
- stakingIdFactory = stakingIdFactory,
- )
- }
-
@Provides
@Singleton
fun provideFetchPendingTransactionsUseCase(
@@ -75,32 +53,6 @@ internal object TokensDomainModule {
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
}
- @Provides
- @Singleton
- fun provideGetTokenListUseCase(
- currenciesRepository: CurrenciesRepository,
- currenciesStatusesOperations: BaseCurrencyStatusOperations,
- ): GetTokenListUseCase {
- return GetTokenListUseCase(
- currenciesRepository = currenciesRepository,
- currenciesStatusesOperations = currenciesStatusesOperations,
- )
- }
-
- @Provides
- @Singleton
- fun provideRemoveCurrencyUseCase(
- currenciesRepository: CurrenciesRepository,
- walletManagersFacade: WalletManagersFacade,
- multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
- ): RemoveCurrencyUseCase {
- return RemoveCurrencyUseCase(
- currenciesRepository = currenciesRepository,
- walletManagersFacade = walletManagersFacade,
- multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
- )
- }
-
@Provides
@Singleton
fun provideGetCurrencyUseCase(
@@ -113,20 +65,6 @@ internal object TokensDomainModule {
)
}
- @Provides
- @Singleton
- fun provideGetAllWalletsCryptoCurrencyStatusesUseCase(
- currenciesRepository: CurrenciesRepository,
- currencyStatusOperations: BaseCurrencyStatusOperations,
- dispatchers: CoroutineDispatcherProvider,
- ): GetAllWalletsCryptoCurrencyStatusesUseCase {
- return GetAllWalletsCryptoCurrencyStatusesUseCase(
- currenciesRepository = currenciesRepository,
- currencyStatusOperations = currencyStatusOperations,
- dispatchers = dispatchers,
- )
- }
-
@Provides
@Singleton
fun provideGetCurrencyWarningsUseCase(
@@ -176,34 +114,6 @@ internal object TokensDomainModule {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
- @Provides
- @Singleton
- fun provideToggleTokenListGroupingUseCase(
- dispatchers: CoroutineDispatcherProvider,
- ): ToggleTokenListGroupingUseCase {
- return ToggleTokenListGroupingUseCase(dispatchers)
- }
-
- @Provides
- @Singleton
- fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
- return ToggleTokenListSortingUseCase(dispatchers)
- }
-
- @Provides
- @Singleton
- fun provideApplyTokenListSortingUseCase(
- currenciesRepository: CurrenciesRepository,
- multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
- dispatchers: CoroutineDispatcherProvider,
- ): ApplyTokenListSortingUseCase {
- return ApplyTokenListSortingUseCase(
- currenciesRepository = currenciesRepository,
- multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
- dispatchers = dispatchers,
- )
- }
-
@Provides
@Singleton
fun provideGetCryptoCurrencyActionsUseCase(
@@ -308,14 +218,6 @@ internal object TokensDomainModule {
)
}
- @Provides
- @Singleton
- fun provideGetWalletTotalBalanceUseCase(
- currenciesStatusesOperations: BaseCurrencyStatusOperations,
- ): GetWalletTotalBalanceUseCase {
- return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
- }
-
@Provides
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
@@ -350,7 +252,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
- return CachedCurrenciesStatusesOperations(
+ return BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
@@ -363,12 +265,6 @@ internal object TokensDomainModule {
)
}
- @Provides
- @Singleton
- fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
- return GetCryptoCurrenciesUseCase(currenciesRepository)
- }
-
@Provides
@Singleton
fun provideWalletBalanceFetcher(
@@ -378,6 +274,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
+ paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@@ -388,6 +285,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
+ paymentAccountStatusFetcher = paymentAccountStatusFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)
diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
index 52ecf3a396..9cdd6d66dc 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
@@ -78,12 +78,14 @@ internal object TransactionDomainModule {
walletManagersFacade: WalletManagersFacade,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
+ tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}
@@ -92,10 +94,12 @@ internal object TransactionDomainModule {
fun provideRetryTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
+ tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): RetryIncompleteTransactionUseCase {
return RetryIncompleteTransactionUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
+ getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}
@@ -104,10 +108,12 @@ internal object TransactionDomainModule {
fun provideOpenTrustlineUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
+ tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): OpenTrustlineUseCase {
return OpenTrustlineUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
+ getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
index 5e55ebdd54..c63293cda2 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
@@ -66,6 +66,12 @@ internal object WalletsDomainModule {
return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository)
}
+ @Provides
+ @Singleton
+ fun provideGetWalletIconUseCase(walletsRepository: WalletsRepository): GetWalletIconUseCase {
+ return GetWalletIconUseCase(walletsRepository = walletsRepository)
+ }
+
@Provides
@Singleton
fun providesGetSelectedWalletSyncUseCase(
diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
index aea6c8a351..caf25e2b96 100644
--- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
@@ -18,7 +18,10 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.services.secure.SecureStorage
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
+import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
+import com.tangem.core.analytics.models.AnalyticsParam
+import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.finisher.AppFinisher
@@ -53,11 +56,7 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
-import com.tangem.tap.derivationsFinder
-import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
-import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
-import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
-import com.tangem.tap.domain.tasks.product.ScanProductTask
+import com.tangem.tap.domain.tasks.product.*
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
@@ -85,6 +84,8 @@ internal class DefaultTangemSdkManager(
private val appFinisher: AppFinisher,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
+ private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
+ private val analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider,
) : TangemSdkManager {
@@ -173,7 +174,7 @@ internal class DefaultTangemSdkManager(
runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
- derivationsFinder = derivationsFinder,
+ blockchainToDeriveFinder = blockchainToDeriveFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
@@ -473,6 +474,9 @@ internal class DefaultTangemSdkManager(
title = resourceReference(R.string.alert_button_request_support),
onClick = {
coroutineScope.launch {
+ analyticsEventHandler.send(
+ Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.SignIn),
+ )
sendFeedbackEmailUseCase(FeedbackEmailType.BiometricsAuthenticationFailed)
}
},
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt
new file mode 100644
index 0000000000..9118279751
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt
@@ -0,0 +1,74 @@
+package com.tangem.tap.domain.tasks.product
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.derivation.DerivationStyle
+import com.tangem.blockchainsdk.utils.fromNetworkId
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.data.common.account.WalletAccountsFetcher
+import com.tangem.data.wallets.derivations.BlockchainToDerive
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.domain.wallets.builder.UserWalletIdBuilder
+import com.tangem.domain.wallets.derivations.derivationStyleProvider
+import com.tangem.tap.features.demo.DemoHelper
+import javax.inject.Inject
+
+/**
+ * Finder of blockchains to derive.
+ * Returns only saved, default or demo blockchains without any additional logic
+ * (no cardano/ethereum additions or unnecessary blockchain removals).
+ */
+class BlockchainToDeriveFinder @Inject constructor(
+ private val walletAccountsFetcher: WalletAccountsFetcher,
+) {
+
+ suspend fun find(card: CardDTO): Set {
+ if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
+ val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
+
+ val derivationStyle = card.derivationStyleProvider.getDerivationStyle()
+
+ val blockchains = getBlockchains(userWalletId).ifEmpty {
+ if (DemoHelper.isDemoCardId(card.cardId)) {
+ getDemoBlockchains(derivationStyle, card.cardId)
+ } else {
+ getDefaultBlockchains(derivationStyle)
+ }
+ }
+
+ return blockchains
+ }
+
+ private suspend fun getBlockchains(userWalletId: UserWalletId): Set {
+ return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty()
+ .flatMap { accountDTO ->
+ accountDTO.tokens.orEmpty()
+ .filter { it.contractAddress == null }
+ }
+ .mapNotNull { coin ->
+ val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
+ val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null
+
+ BlockchainToDerive(blockchain, derivationPath)
+ }
+ .toSet()
+ }
+
+ private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set {
+ return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
+ }
+
+ private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set {
+ val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
+ return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
+ }
+
+ private fun Set.mapToBlockchainsWithDerivations(
+ derivationStyle: DerivationStyle?,
+ ): Set {
+ return mapNotNullTo(hashSetOf()) { blockchain ->
+ val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
+ BlockchainToDerive(blockchain, derivationPath)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt
deleted file mode 100644
index eb99c14c88..0000000000
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.tangem.tap.domain.tasks.product
-
-import com.tangem.blockchain.blockchains.cardano.CardanoUtils
-import com.tangem.blockchain.common.Blockchain
-import com.tangem.blockchain.common.derivation.DerivationStyle
-import com.tangem.blockchainsdk.utils.fromNetworkId
-import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.datasource.local.token.UserTokensResponseStore
-import com.tangem.domain.wallets.derivations.DerivationStyleProvider
-import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation
-import com.tangem.domain.models.scan.CardDTO
-import com.tangem.domain.models.wallet.UserWalletId
-import com.tangem.domain.wallets.builder.UserWalletIdBuilder
-import com.tangem.tap.features.demo.DemoHelper
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.coroutines.withContext
-
-internal data class BlockchainToDerive(
- val blockchain: Blockchain,
- val derivationPath: DerivationPath?,
-)
-
-// FIXME: May be move to DI, currently unnecessary
-internal class DerivationsFinder(
- private val userTokensResponseStore: UserTokensResponseStore,
- private val dispatchers: CoroutineDispatcherProvider,
-) {
-
- suspend fun findBlockchainsToDerive(
- card: CardDTO,
- derivationStyleProvider: DerivationStyleProvider,
- ): Set {
- if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
- val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
- val derivationStyle = derivationStyleProvider.getDerivationStyle()
-
- val blockchains = withContext(dispatchers.io) {
- getBlockchains(userWalletId)
- }.ifEmpty {
- if (DemoHelper.isDemoCardId(card.cardId)) {
- getDemoBlockchains(derivationStyle, card.cardId)
- } else {
- getDefaultBlockchains(derivationStyle)
- }
- }
-
- // we should generate second key for cardano
- // because cardano address generation for wallet2 requires keys from 2 derivations
- // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
- blockchains.addSecondCardanoDerivationIfPresent()
-
- if (card.settings.isHDWalletAllowed) {
- blockchains.addEthereumBlockchains(derivationStyle)
- }
-
- // pay attention to this
- if (!card.hasOldStyleDerivation) {
- blockchains.removeUnnecessaryBlockchains()
- }
-
- return blockchains
- }
-
- private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet {
- val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens
- ?: return hashSetOf()
-
- return responseTokens.asSequence()
- .filter { it.contractAddress == null }
- .mapNotNull { coin ->
- val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
- val derivationPath = coin.derivationPath?.let(::DerivationPath)
-
- BlockchainToDerive(blockchain, derivationPath)
- }
- .toMutableSet()
- }
-
- // TODO: Move to user wallet config
- private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet {
- return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
- }
-
- // TODO: Move to user wallet config
- private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet {
- val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
-
- return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
- }
-}
-
-private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) {
- val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet)
- .mapToBlockchainsWithDerivations(derivationStyle)
-
- addAll(ethereumBlockchains)
-}
-
-private fun MutableSet.removeUnnecessaryBlockchains() {
- val unnecessaryBlockchains = listOf(
- Blockchain.BSC, Blockchain.BSCTestnet,
- Blockchain.Polygon, Blockchain.PolygonTestnet,
- Blockchain.RSK,
- Blockchain.Fantom, Blockchain.FantomTestnet,
- Blockchain.Avalanche, Blockchain.AvalancheTestnet,
- )
-
- removeAll { it.blockchain in unnecessaryBlockchains }
-}
-
-private fun MutableSet.addSecondCardanoDerivationIfPresent() {
- val cardanoDerivation = this
- .firstOrNull { it.blockchain == Blockchain.Cardano }
- ?.derivationPath
- ?: return
-
- val secondCardanoBlockchain = BlockchainToDerive(
- blockchain = Blockchain.Cardano,
- derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation),
- )
-
- add(secondCardanoBlockchain)
-}
-
-private fun Set.mapToBlockchainsWithDerivations(
- derivationStyle: DerivationStyle?,
-): MutableSet {
- return mapTo(hashSetOf()) { blockchain ->
- BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
index 4c334fd027..eb14c89bb4 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
@@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.wallets.derivations.DerivationStyleProvider
+import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.card.common.TwinsHelper
-import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities
-import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
@@ -45,11 +43,10 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import kotlin.collections.set
internal class ScanProductTask(
private val card: Card?,
- private val derivationsFinder: DerivationsFinder?,
+ private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
@@ -81,7 +78,7 @@ internal class ScanProductTask(
readVisaCard(
session = session,
cardDto = cardDto,
- scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
+ scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder),
callback = callback,
)
return
@@ -89,7 +86,7 @@ internal class ScanProductTask(
val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor()
- else -> ScanWalletProcessor(derivationsFinder)
+ else -> ScanWalletProcessor(blockchainToDeriveFinder)
}
commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) {
@@ -170,7 +167,7 @@ internal class ScanProductTask(
}
private class ScanWalletProcessor(
- private val derivationsFinder: DerivationsFinder?,
+ private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
) : ProductCommandProcessor {
var primaryCard: PrimaryCard? = null
@@ -293,7 +290,6 @@ private class ScanWalletProcessor(
callback: (result: CompletionResult) -> Unit,
) {
val productType = getWalletProductType(card)
- val config = CardConfig.createConfig(card)
scope.launch {
val scanResponse = ScanResponse(
card = card,
@@ -301,8 +297,7 @@ private class ScanWalletProcessor(
walletData = session.environment.walletData,
primaryCard = primaryCard,
)
- val derivations =
- collectDerivations(card, config, scanResponse.derivationStyleProvider)
+ val derivations = collectDerivations(card, scanResponse)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse))
return@launch
@@ -332,32 +327,13 @@ private class ScanWalletProcessor(
private suspend fun collectDerivations(
card: CardDTO,
- config: CardConfig,
- derivationStyleProvider: DerivationStyleProvider,
+ scanResponse: ScanResponse,
): Map> {
- val derivations = mutableMapOf>()
- val blockchains = derivationsFinder
- ?.findBlockchainsToDerive(card, derivationStyleProvider)
- ?: return derivations
+ val blockchains = blockchainToDeriveFinder
+ ?.find(card)
+ ?: return emptyMap()
- blockchains.forEach { blockchain ->
- val curve = config.primaryCurve(blockchain.blockchain)
- val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
- if (wallet.chainCode == null) return@forEach
-
- val key = wallet.publicKey.toMapKey()
- val path = blockchain.derivationPath
- if (path != null) {
- val addedDerivations = derivations[key]
- if (addedDerivations != null) {
- derivations[key] = addedDerivations + path
- } else {
- derivations[key] = listOf(path)
- }
- }
- }
-
- return derivations
+ return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains)
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt
index 444b1f4032..4e27097a9d 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt
@@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.tokens.TokensFeatureToggles
internal class DefaultTokensFeatureToggles(
- @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager,
-) : TokensFeatureToggles
\ No newline at end of file
+ private val featureTogglesManager: FeatureTogglesManager,
+) : TokensFeatureToggles {
+
+ override val isMultiAddressUtxoEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
index 0f7ba29f04..c851c06bdf 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
@@ -28,7 +28,7 @@ class FinalizeTwinTask(
is CompletionResult.Success ->
ScanProductTask(
card = readResult.data,
- derivationsFinder = null,
+ blockchainToDeriveFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false,
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt
index c75cbbbed9..80cbe137b9 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt
@@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository(
.map { wallets.updateWith(it) }
}
.doOnSuccess { loadedWallets ->
- userWallets.update { toUpdate ->
+ userWallets.update { _ ->
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId }
?: loadedWallets.firstOrNull()?.also {
@@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository(
}
}
- @Suppress("CyclomaticComplexMethod")
+ @Suppress("CyclomaticComplexMethod", "LongMethod")
override suspend fun unlock(
userWalletId: UserWalletId,
unlockMethod: UserWalletsListRepository.UnlockMethod,
@@ -317,7 +317,13 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo ->
- updateWallets { it?.updateWith(sensitiveInfo) }
+ updateWallets { wallets ->
+ // It is necessary to update derivations because when scanning we obtain the missing keys
+ wallets?.updateWith(
+ walletIdToSensitiveInformation = sensitiveInfo,
+ walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
+ )
+ }
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
index c9184f1335..51aefb10ab 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
@@ -1,8 +1,10 @@
package com.tangem.tap.domain.userWalletList.utils
+import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
+import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
@@ -72,7 +74,10 @@ internal fun List.toUserWallets(): List
return this.map { it.toUserWallet() }
}
-internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
+internal fun UserWallet.updateWith(
+ sensitiveInformation: UserWalletSensitiveInformation,
+ derivedKeys: Map?,
+): UserWallet {
return when (this) {
is UserWallet.Cold -> {
copy(
@@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
card = scanResponse.card.copy(
wallets = requireNotNull(sensitiveInformation.wallets),
),
+ derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
@@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
internal fun List.updateWith(
walletIdToSensitiveInformation: Map,
+ walletIdToDerivedKeys: Map>? = null,
): List {
return if (walletIdToSensitiveInformation.isEmpty()) {
this
} else {
this.map { wallet ->
- walletIdToSensitiveInformation[wallet.walletId]
- ?.let(wallet::updateWith)
- ?: wallet
+ val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
+ val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
+
+ if (sensitiveInformation != null) {
+ wallet.updateWith(sensitiveInformation, derivedKeys)
+ } else {
+ wallet
+ }
}
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt
index 1d9a6b2f6e..22e94ae7d3 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt
@@ -222,7 +222,7 @@ internal class CardSettingsModel @Inject constructor(
val card = scanResponse.card
modelScope.launch {
- val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
+ val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true
store.dispatchNavigationAction {
push(
route = AppRoute.ResetToFactory(
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
deleted file mode 100644
index 6cbc795c53..0000000000
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.tangem.tap.features.wallet.redux.middlewares
-
-import com.tangem.common.routing.AppRouter
-import com.tangem.core.analytics.Analytics
-import com.tangem.domain.models.network.NetworkAddress
-import com.tangem.domain.tokens.legacy.TradeCryptoAction
-import com.tangem.tap.common.analytics.events.Token
-import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
-import com.tangem.tap.common.extensions.dispatchNavigationAction
-import com.tangem.tap.common.extensions.dispatchOpenUrl
-import com.tangem.tap.common.extensions.inject
-import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.features.demo.DemoHelper
-import com.tangem.tap.proxy.redux.DaggerGraphState
-import com.tangem.tap.store
-import org.rekotlin.Middleware
-
-@Deprecated("Will be removed soon")
-object TradeCryptoMiddleware {
-
- val middleware: Middleware = { _, appState ->
- { nextDispatch ->
- { action ->
- if (action is TradeCryptoAction) {
- handle(appState, action)
- }
- nextDispatch(action)
- }
- }
- }
-
- private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
- if (DemoHelper.tryHandle(state)) return
-
- when (action) {
- is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
- is TradeCryptoAction.Sell -> proceedSellAction(action)
- }
- }
-
- private fun proceedSellAction(action: TradeCryptoAction.Sell) {
- val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
- ?.defaultAddress
- ?.let(NetworkAddress.Address::value)
- ?: return
- val currency = action.cryptoCurrencyStatus.currency
-
- store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl(
- cryptoCurrency = currency,
- fiatCurrencyName = action.appCurrencyCode,
- walletAddress = networkAddress,
- isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
- )?.let { url ->
- store.dispatchOpenUrl(url)
- Analytics.send(Token.Withdraw.ScreenOpened())
- }
- }
-
- private fun openReceiptUrl(transactionId: String) {
- store.dispatchNavigationAction(AppRouter::pop)
-
- val sellService = store.inject(DaggerGraphState::appStateHolder).sellService
- sellService?.getSellCryptoReceiptUrl(transactionId = transactionId)
- ?.let(store::dispatchOpenUrl)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
index 1c102c9e0c..027441c202 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
@@ -3,7 +3,7 @@ package com.tangem.tap.network.auth
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.ApiEnvironment
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.Provider
@@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend
internal class DefaultAuthProvider(
private val userWalletsListRepository: UserWalletsListRepository,
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
) : AuthProvider {
override suspend fun getCardPublicKey(): String {
@@ -47,11 +47,11 @@ internal class DefaultAuthProvider(
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
- -> environmentConfigStorage.getConfigSync().tangemApiKeyDev
+ -> environmentConfig.tangemApiKeyDev
ApiEnvironment.STAGE_2,
ApiEnvironment.STAGE,
- -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
- ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
+ -> environmentConfig.tangemApiKeyStage
+ ApiEnvironment.PROD -> environmentConfig.tangemApiKey
} ?: error("No tangem tech api config provided")
}
}
@@ -60,8 +60,8 @@ internal class DefaultAuthProvider(
return ProviderSuspend {
when (apiEnvironment.invoke()) {
ApiEnvironment.DEV,
- -> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev
- ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey
+ -> environmentConfig.gaslessTxApiKeyDev
+ ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
} ?: error("No gasless tx api config provided")
}
diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
index 93b0595647..9ed1541c3e 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
@@ -1,15 +1,15 @@
package com.tangem.tap.network.auth
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
) : P2PEthPoolAuthProvider {
override fun getApiKey(): String {
- val keys = environmentConfigStorage.getConfigSync().p2pApiKey
+ val keys = environmentConfig.p2pApiKey
?: error("No P2P api keys provided")
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt
index ba0534ae88..079cad327a 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt
@@ -1,13 +1,13 @@
package com.tangem.tap.network.auth
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.lib.auth.StakeKitAuthProvider
internal class DefaultStakeKitAuthProvider(
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
) : StakeKitAuthProvider {
override fun getApiKey(): String {
- return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided")
+ return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt
index e6cba79706..6ef9e06f96 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
@@ -22,11 +22,11 @@ internal class AuthModule {
@Singleton
fun provideAuthProvider(
userWalletsListRepository: UserWalletsListRepository,
- environmentConfigStorage: EnvironmentConfigStorage,
+ environmentConfig: EnvironmentConfig,
): AuthProvider {
return DefaultAuthProvider(
userWalletsListRepository = userWalletsListRepository,
- environmentConfigStorage = environmentConfigStorage,
+ environmentConfig = environmentConfig,
)
}
@@ -38,14 +38,14 @@ internal class AuthModule {
@Provides
@Singleton
- fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider {
- return DefaultStakeKitAuthProvider(environmentConfigStorage)
+ fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider {
+ return DefaultStakeKitAuthProvider(environmentConfig)
}
@Provides
@Singleton
- fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider {
- return DefaultP2PEthPoolAuthProvider(environmentConfigStorage)
+ fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider {
+ return DefaultP2PEthPoolAuthProvider(environmentConfig)
}
@Provides
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt
index 82aa59fe63..4075e7dd57 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt
@@ -21,6 +21,4 @@ interface SellService {
walletAddress: String,
isDarkTheme: Boolean,
): String?
-
- fun getSellCryptoReceiptUrl(transactionId: String): String?
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
index 60491ab4c7..742e44ec5e 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
@@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
-import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec
class MoonPayService(
private val api: MoonPayApi,
- private val apiKeyProvider: Provider,
- private val secretKeyProvider: Provider,
+ private val apiKey: String,
+ private val secretKey: String,
private val userWalletProvider: () -> UserWallet?,
) : SellService {
@@ -47,18 +46,18 @@ class MoonPayService(
_initializationStatus.value = lceLoading()
performRequest {
- val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
+ val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> {
- Timber.e("Failed to load user status", result.error)
+ Timber.e(result.error, "Failed to load user status")
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
- val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
+ val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> {
- Timber.e("Failed to load currencies", result.error)
+ Timber.e(result.error, "Failed to load currencies")
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@@ -163,7 +162,7 @@ class MoonPayService(
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
- .appendQueryParameter("apiKey", apiKeyProvider())
+ .appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
@@ -177,17 +176,9 @@ class MoonPayService(
return uri.build().toString()
}
- override fun getSellCryptoReceiptUrl(transactionId: String): String {
- return Uri.Builder()
- .scheme(SCHEME)
- .authority(URL_SELL)
- .appendPath("transaction_receipt")
- .appendQueryParameter("transactionId", transactionId).build().toString()
- }
-
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
- val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
+ val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
index 133c325afc..d86379f650 100644
--- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
@@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.connection.NetworkConnectionManager
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
@@ -63,7 +62,6 @@ data class DaggerGraphState(
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
val transactionSignerFactory: TransactionSignerFactory? = null,
- val environmentConfigStorage: EnvironmentConfigStorage? = null,
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
val onboardingRepository: OnboardingRepository? = null,
val excludedBlockchains: ExcludedBlockchains? = null,
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
index aa9b5efd77..4131a1c99f 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
@@ -2,7 +2,6 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
-import com.tangem.domain.models.PortfolioId
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent
@@ -18,7 +17,6 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryRoute
-import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent
@@ -26,7 +24,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
-import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
@@ -67,7 +64,6 @@ internal class ChildFactory @Inject constructor(
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
- private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
private val onrampComponentFactory: OnrampComponent.Factory,
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
@@ -117,7 +113,6 @@ internal class ChildFactory @Inject constructor(
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
- private val feedFeatureToggle: FeedFeatureToggle,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@@ -147,11 +142,7 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
}
- val mode = when (val portfolio = route.portfolioId) {
- is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId)
- is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId)
- null -> ManageTokensMode.None
- }
+ val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None
createComponentChild(
context = context,
@@ -193,39 +184,21 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.MarketsTokenDetails -> {
- if (feedFeatureToggle.isFeedEnabled) {
- createComponentChild(
- context = context,
- params = FeedEntryRoute.MarketTokenDetails(
- token = route.token,
- appCurrency = route.appCurrency,
- shouldShowPortfolio = route.shouldShowPortfolio,
- analyticsParams = route.analyticsParams?.let { params ->
- FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
- blockchain = params.blockchain,
- source = params.source,
- )
- },
- ),
- componentFactory = feedEntryComponentFactory,
- )
- } else {
- createComponentChild(
- context = context,
- params = MarketsTokenDetailsComponent.Params(
- token = route.token,
- appCurrency = route.appCurrency,
- shouldShowPortfolio = route.shouldShowPortfolio,
- analyticsParams = route.analyticsParams?.let { params ->
- MarketsTokenDetailsComponent.AnalyticsParams(
- blockchain = params.blockchain,
- source = params.source,
- )
- },
- ),
- componentFactory = marketsTokenDetailsComponentFactory,
- )
- }
+ createComponentChild(
+ context = context,
+ params = FeedEntryRoute.MarketTokenDetails(
+ token = route.token,
+ appCurrency = route.appCurrency,
+ shouldShowPortfolio = route.shouldShowPortfolio,
+ analyticsParams = route.analyticsParams?.let { params ->
+ FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
+ blockchain = params.blockchain,
+ source = params.source,
+ )
+ },
+ ),
+ componentFactory = feedEntryComponentFactory,
+ )
}
is AppRoute.Onramp -> {
createComponentChild(
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
index 27c065e869..4bc37fc48e 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
@@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
-import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor(
private val promoDeepLink: PromoDeeplinkHandler.Factory,
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
- private val feedFeatureToggle: FeedFeatureToggle,
) {
private val permittedAppRoute = MutableStateFlow(false)
@@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor(
onboardVisaDeepLink.create(deeplinkUri)
return
}
- deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> {
+ deeplinkUri.path?.startsWith("/news") == true -> {
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
return
}
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 3b1aea3367..ad92c990d1 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,21 +1,4 @@
-
-
-
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground_base.xml b/app/src/main/res/drawable/ic_launcher_foreground_base.xml
new file mode 100644
index 0000000000..3b1aea3367
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground_base.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 8b20aae928..8cc7aedc0a 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,5 +2,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/mocked/res/drawable/ic_launcher_foreground.xml b/app/src/mocked/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..ba2ae549e5
--- /dev/null
+++ b/app/src/mocked/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt
new file mode 100644
index 0000000000..16ba7e686f
--- /dev/null
+++ b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt
@@ -0,0 +1,134 @@
+package com.tangem.tap.data
+
+import com.google.common.truth.Truth.assertThat
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
+import com.tangem.tap.network.exchangeServices.SellService
+import io.mockk.*
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+internal class DefaultOfframpRepositoryTest {
+
+ private val sellService: SellService = mockk()
+ private val repository = DefaultOfframpRepository(sellService)
+
+ private val cryptoCurrency: CryptoCurrency = mockk()
+ private val fiatCurrencyCode = "USD"
+ private val walletAddress = "0x1234567890abcdef"
+
+ @BeforeEach
+ fun setUp() {
+ mockkObject(MutableAppThemeModeHolder)
+ }
+
+ @AfterEach
+ fun tearDown() {
+ clearMocks(sellService)
+ unmockkObject(MutableAppThemeModeHolder)
+ }
+
+ @Test
+ fun `getOfframpUrl should return url when sellService returns url with light theme`() {
+ // Arrange
+ val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light"
+ every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
+ every {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = false,
+ )
+ } returns expectedUrl
+
+ // Act
+ val result = repository.getOfframpUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyCode = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ )
+
+ // Assert
+ assertThat(result).isEqualTo(expectedUrl)
+
+ verify(exactly = 1) {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = false,
+ )
+ }
+ }
+
+ @Test
+ fun `getOfframpUrl should return url when sellService returns url with dark theme`() {
+ // Arrange
+ val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark"
+ every { MutableAppThemeModeHolder.isDarkThemeActive } returns true
+ every {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = true,
+ )
+ } returns expectedUrl
+
+ // Act
+ val result = repository.getOfframpUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyCode = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ )
+
+ // Assert
+ assertThat(result).isEqualTo(expectedUrl)
+
+ verify(exactly = 1) {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = true,
+ )
+ }
+ }
+
+ @Test
+ fun `getOfframpUrl should return null when sellService returns null`() {
+ // Arrange
+ every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
+ every {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = false,
+ )
+ } returns null
+
+ // Act
+ val result = repository.getOfframpUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyCode = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ )
+
+ // Assert
+ assertThat(result).isNull()
+
+ verify(exactly = 1) {
+ sellService.getUrl(
+ cryptoCurrency = cryptoCurrency,
+ fiatCurrencyName = fiatCurrencyCode,
+ walletAddress = walletAddress,
+ isDarkTheme = false,
+ )
+ }
+ }
+}
diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt
new file mode 100644
index 0000000000..5be7a5b371
--- /dev/null
+++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt
@@ -0,0 +1,252 @@
+package com.tangem.tap.domain.tasks.product
+
+import com.google.common.truth.Truth
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.derivation.DerivationStyle
+import com.tangem.blockchainsdk.utils.toNetworkId
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.data.common.account.WalletAccountsFetcher
+import com.tangem.data.wallets.derivations.BlockchainToDerive
+import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
+import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
+import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.wallet.UserWalletId
+import io.mockk.*
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+
+/**
+[REDACTED_AUTHOR]
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class BlockchainToDeriveFinderTest {
+
+ private val walletAccountsFetcher = mockk()
+ private val finder = BlockchainToDeriveFinder(
+ walletAccountsFetcher = walletAccountsFetcher,
+ )
+
+ @AfterEach
+ fun tearDown() {
+ clearMocks(walletAccountsFetcher)
+ }
+
+ @Test
+ fun `GIVEN card is not HD wallet THEN return empty set`() = runTest {
+ // Arrange
+ val card = mockk {
+ every { this@mockk.settings.isHDWalletAllowed } returns false
+ }
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ Truth.assertThat(actual).isEmpty()
+ }
+
+ @Test
+ fun `GIVEN card has empty wallets THEN return empty set`() = runTest {
+ // Arrange
+ val card = mockk {
+ every { this@mockk.settings.isHDWalletAllowed } returns true
+ every { this@mockk.wallets } returns emptyList()
+ }
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ Truth.assertThat(actual).isEmpty()
+ }
+
+ @Test
+ fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest {
+ // Arrange
+ val card = createCardDTO()
+
+ val response = createResponse(Blockchain.Bitcoin)
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = setOf(
+ createExpected(Blockchain.Bitcoin),
+ )
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ @Test
+ fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest {
+ // Arrange
+ val demoCardId = "AC01000000045754"
+ val card = createCardDTO(cardId = demoCardId)
+
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = setOf(
+ createExpected(Blockchain.Bitcoin),
+ createExpected(Blockchain.Ethereum),
+ createExpected(Blockchain.Dogecoin),
+ createExpected(Blockchain.Solana),
+ )
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ @Test
+ fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest {
+ // Arrange
+ val demoCardId = "DE00"
+ val card = createCardDTO(cardId = demoCardId)
+
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = setOf(
+ createExpected(Blockchain.Bitcoin),
+ createExpected(Blockchain.Ethereum),
+ createExpected(Blockchain.Dogecoin),
+ )
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ @Test
+ fun `GIVEN empty store THEN return default blockchains`() = runTest {
+ // Arrange
+ val card = createCardDTO()
+
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = setOf(
+ createExpected(Blockchain.Bitcoin),
+ createExpected(Blockchain.Ethereum),
+ )
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ @Test
+ fun `GIVEN saved cardano THEN return only cardano`() = runTest {
+ // Arrange
+ val card = createCardDTO()
+
+ val response = createResponse(Blockchain.Cardano)
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = setOf(
+ createExpected(Blockchain.Cardano),
+ )
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ @Test
+ fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest {
+ // Arrange
+ val card = createCardDTO()
+
+ val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon)
+
+ val response = createResponse(*blockchains.toTypedArray())
+
+ coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
+
+ // Act
+ val actual = finder.find(card)
+
+ // Assert
+ val expected = blockchains.mapTo(hashSetOf(), ::createExpected)
+
+ Truth.assertThat(actual).containsExactlyElementsIn(expected)
+
+ coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
+ }
+
+ private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO {
+ val wallet = mockk {
+ every { this@mockk.publicKey } returns byteArrayOf(0)
+ }
+
+ return mockk {
+ every { this@mockk.cardId } returns cardId
+ every { this@mockk.batchId } returns batchId
+ every { this@mockk.settings.isHDWalletAllowed } returns true
+ every { this@mockk.settings.isKeysImportAllowed } returns true
+ every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion(
+ major = 6,
+ minor = 33,
+ patch = 0,
+ type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release,
+ )
+ every { this@mockk.wallets } returns listOf(wallet)
+ }
+ }
+
+ private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse {
+ val tokens = blockchains.map { blockchain ->
+ mockk {
+ every { this@mockk.networkId } returns blockchain.toNetworkId()
+ every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath
+ every { this@mockk.contractAddress } returns null
+ }
+ }
+
+ val account = mockk {
+ every { this@mockk.tokens } returns tokens
+ }
+
+ return mockk {
+ every { this@mockk.accounts } returns listOf(account)
+ }
+ }
+
+ private fun createExpected(
+ blockchain: Blockchain,
+ derivationPath: DerivationPath = blockchain.getDerivationPath(),
+ ): BlockchainToDerive {
+ return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath)
+ }
+
+ private fun Blockchain.getDerivationPath(): DerivationPath {
+ return derivationPath(DerivationStyle.V3)!!
+ }
+
+ private companion object {
+
+ // for byteArrayOf(0)
+ val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7")
+ }
+}
diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
index 19782fca91..4d40266dc8 100644
--- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
@@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
-import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@@ -87,7 +86,6 @@ class DeepLinkFactoryTest {
private val newsDeeplink = mockk(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
- private val feedFeatureToggle = mockk()
private val mockedUri = mockk(relaxed = true)
private val isFromOnNewIntent: Boolean = false
@@ -112,7 +110,6 @@ class DeepLinkFactoryTest {
promoDeepLink = promoDeepLinkFactory,
onboardVisaDeepLink = onboardVisaDeepLink,
newsDetailsDeepLink = newsDeeplink,
- feedFeatureToggle = feedFeatureToggle,
)
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/build.gradle.kts b/build.gradle.kts
index c8864ff496..b61f203435 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,3 @@
-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import java.util.concurrent.ConcurrentHashMap
-
plugins {
alias(deps.plugins.kotlin.android) apply false
alias(deps.plugins.kotlin.jvm) apply false
@@ -33,83 +30,13 @@ interface Injected {
val fs: FileSystemOperations
}
-data class TestStats(
- val total: Long = 0,
- val passed: Long = 0,
- val failed: Long = 0,
- val skipped: Long = 0,
-)
-
-val testResultsByModule = ConcurrentHashMap()
-
// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules
val unitTest by tasks.registering {
group = "verification"
description = "Run unit tests for debug/googleDebug variant and all JVM modules"
-
- doLast {
- if (testResultsByModule.isNotEmpty()) {
- val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats ->
- TestStats(
- total = acc.total + stats.total,
- passed = acc.passed + stats.passed,
- failed = acc.failed + stats.failed,
- skipped = acc.skipped + stats.skipped,
- )
- }
-
- println("\n" + "=".repeat(80))
- println("TEST SUMMARY")
- println("=".repeat(80))
-
- testResultsByModule.toSortedMap().forEach { (module, stats) ->
- println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)")
- }
-
- println("-".repeat(80))
- println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules")
- println(" Passed: ${totalStats.passed}")
- println(" Failed: ${totalStats.failed}")
- println(" Skipped: ${totalStats.skipped}")
- println("=".repeat(80))
- }
- }
}
-// Test Logging and testCI dependencies
subprojects {
- tasks.withType().configureEach {
- println("Test task scheduled: $path")
-
- testLogging {
- exceptionFormat = TestExceptionFormat.FULL
- showStandardStreams = true
-
- afterSuite(KotlinClosure2({ desc, result ->
- if (desc.parent == null) { // will match the outermost suite
- testResultsByModule[path] = TestStats(
- total = result.testCount,
- passed = result.successfulTestCount,
- failed = result.failedTestCount,
- skipped = result.skippedTestCount,
- )
-
- val output =
- "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
- val startItem = "| "
- val endItem = " |"
- val repeatLength = startItem.length + output.length + endItem.length
- println(
- "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(
- repeatLength
- )
- )
- }
- }))
- }
- }
-
- // Register testCI dependencies
// App module
plugins.withId("com.android.application") {
afterEvaluate {
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
index a514a89100..07b256a7eb 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -7,13 +7,12 @@ import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
-import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.markets.TokenMarketParams
-import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
+import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
@@ -21,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.pay.TangemPayDetailsConfig
+import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.tokens.model.details.NavigationAction
import kotlinx.serialization.Serializable
@@ -128,8 +128,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val source: Source,
- val portfolioId: PortfolioId? = null,
- ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
+ val accountId: AccountId? = null,
+ ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${accountId?.value}") {
/**
* Source of launching the screen.
@@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class EditAccount(
- val account: Account,
+ val account: Account.CryptoPortfolio,
) : AppRoute(path = "/edit_account/${account.accountId.value}")
@Serializable
data class AccountDetails(
- val account: Account,
+ val account: Account.CryptoPortfolio,
) : AppRoute(path = "/account_details/${account.accountId.value}")
@Serializable
diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt
index 64554a30e0..1830c50c92 100644
--- a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt
+++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt
@@ -1,31 +1,11 @@
package com.tangem.common
-import com.tangem.utils.SupportedLanguages.CHINESE
-import com.tangem.utils.SupportedLanguages.ENGLISH
-import com.tangem.utils.SupportedLanguages.FRANCH
-import com.tangem.utils.SupportedLanguages.GERMAN
-import com.tangem.utils.SupportedLanguages.JAPANESE
-import java.util.Locale
-
object TangemSiteShareUrlBuilder {
private const val BASE_URL = "https://tangem.com"
private const val CRYPTOCURRENCIES_PATH = "cryptocurrencies"
- @Deprecated("Should use CHINESE from SupportedLanguages, but the site expects zh-Hans in the URL path")
- private const val CHINESE_SITE_LOCALE = "zh-Hans"
-
- @Deprecated("Should rely on SupportedLanguages instead of maintaining a separate list")
- private val siteLocales = mapOf(
- ENGLISH to ENGLISH,
- FRANCH to FRANCH,
- GERMAN to GERMAN,
- JAPANESE to JAPANESE,
- CHINESE to CHINESE_SITE_LOCALE,
- )
-
fun shareUrl(tokenId: String): String {
- val locale = siteLocales[Locale.getDefault().language] ?: ENGLISH
- return "$BASE_URL/$locale/$CRYPTOCURRENCIES_PATH/$tokenId"
+ return "$BASE_URL/$CRYPTOCURRENCIES_PATH/$tokenId"
}
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt
index 6dc7f0d926..ab45c7159d 100644
--- a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt
+++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt
@@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine
object TangemSiteUrlBuilder {
+ const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10"
+
suspend fun getUtmTags(campaign: String?): String {
val langCode = Locale.getDefault().language
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()
diff --git a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt
index a4a646a6ab..973f6c6895 100644
--- a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt
+++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt
@@ -73,7 +73,7 @@ object MockScanResponseFactory {
CardDTO.Wallet(
CardWallet(
publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name
- chainCode = null,
+ chainCode = ByteArray(32), // chainCode must not be null for HD wallets
curve = curve,
settings = createSettings(),
totalSignedHashes = null,
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
index 943fb98735..1955e4b9a0 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
@@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter(
)
return TokenItemState.Content(
id = account.accountId.toItemId(),
- iconState = AccountIconItemStateConverter.convert(this),
+ iconState = AccountIconItemStateConverter().convert(this),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.toItemId(),
- iconState = AccountIconItemStateConverter.convert(account),
+ iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = account.accountId.toItemId(),
- iconState = AccountIconItemStateConverter.convert(account),
+ iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
index 102be5a837..6cc4b2bf7e 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
@@ -1,11 +1,14 @@
package com.tangem.common.ui.account
+import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.Converter
-object AccountIconItemStateConverter : Converter {
+class AccountIconItemStateConverter(
+ val size: AccountIconSize = AccountIconSize.Default,
+) : Converter {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
is Account.CryptoPortfolio -> when {
@@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
+ size = size,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt
new file mode 100644
index 0000000000..f4d414cae3
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt
@@ -0,0 +1,31 @@
+package com.tangem.common.ui.account
+
+import com.tangem.core.ui.components.token.state.TokenItemState
+import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
+import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
+import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
+import com.tangem.utils.converter.Converter
+import kotlinx.collections.immutable.ImmutableList
+
+class TokensListPortfolioItemConverter(
+ val tokenItemUM: TokenItemState,
+ val isExpanded: Boolean,
+ val isCollapsable: Boolean,
+ val tokens: ImmutableList,
+ val onEmptyAction: PortfolioItemContentUM.Empty.Action? = null,
+) : Converter {
+
+ override fun convert(value: Unit): TokensListItemUM.Portfolio {
+ val content = if (tokens.isEmpty()) {
+ PortfolioItemContentUM.Empty(onEmptyAction)
+ } else {
+ PortfolioItemContentUM.Tokens(tokens)
+ }
+ return TokensListItemUM.Portfolio(
+ tokenItemUM = tokenItemUM,
+ isExpanded = isExpanded,
+ isCollapsable = isCollapsable,
+ content = content,
+ )
+ }
+}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt
deleted file mode 100644
index 89855cf6ce..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.tangem.common.ui.alerts
-
-import com.tangem.common.ui.alerts.models.AlertDemoModeUM
-import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
-import com.tangem.common.ui.alerts.models.AlertUM
-import com.tangem.core.ui.R
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.wrappedList
-import com.tangem.domain.transaction.error.SendTransactionError
-import com.tangem.utils.converter.Converter
-
-class TransactionErrorAlertConverter(
- private val popBackStack: () -> Unit,
- private val onFailedTxEmailClick: (String) -> Unit,
-) : Converter {
- override fun convert(value: SendTransactionError): AlertUM? {
- return when (value) {
- is SendTransactionError.DemoCardError -> AlertDemoModeUM(
- onConfirmClick = popBackStack,
- )
- is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
- code = value.code.toString(),
- cause = null,
- causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
- onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
- )
- is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
- code = value.code.toString(),
- cause = value.message,
- onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
- )
- is SendTransactionError.DataError -> AlertTransactionErrorUM(
- code = "",
- cause = value.message,
- onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
- )
- is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
- code = value.code.orEmpty(),
- cause = value.message.orEmpty(),
- onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
- )
- is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
- code = "",
- cause = value.ex?.localizedMessage,
- onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
- )
- is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM(
- code = "",
- cause = null,
- causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)),
- onConfirmClick = popBackStack,
- )
- else -> null
- }
- }
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt
new file mode 100644
index 0000000000..c5476fa263
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt
@@ -0,0 +1,83 @@
+package com.tangem.common.ui.alerts
+
+import com.tangem.common.ui.R
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.wrappedList
+import com.tangem.core.ui.message.DialogMessage
+import com.tangem.core.ui.message.EventMessageAction
+import com.tangem.domain.transaction.error.SendTransactionError
+import javax.inject.Inject
+
+class TransactionErrorDialogFactory @Inject constructor() {
+
+ fun create(
+ error: SendTransactionError,
+ popBackStack: () -> Unit,
+ onFailedTxEmailClick: (String) -> Unit,
+ ): DialogMessage? {
+ return when (error) {
+ is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack)
+ is SendTransactionError.TangemSdkError -> transactionErrorDialog(
+ causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)),
+ code = error.code.toString(),
+ onConfirmClick = { onFailedTxEmailClick(error.code.toString()) },
+ )
+ is SendTransactionError.BlockchainSdkError -> transactionErrorDialog(
+ cause = error.message,
+ code = error.code.toString(),
+ onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") },
+ )
+ is SendTransactionError.DataError -> transactionErrorDialog(
+ cause = error.message,
+ code = "",
+ onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
+ )
+ is SendTransactionError.NetworkError -> transactionErrorDialog(
+ cause = error.message.orEmpty(),
+ code = error.code.orEmpty(),
+ onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
+ )
+ is SendTransactionError.UnknownError -> transactionErrorDialog(
+ cause = error.ex?.localizedMessage,
+ code = "",
+ onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) },
+ )
+ is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog(
+ causeTextReference = resourceReference(
+ R.string.no_account_polkadot,
+ wrappedList(error.amount),
+ ),
+ code = "",
+ onConfirmClick = popBackStack,
+ )
+ else -> null
+ }
+ }
+
+ private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage(
+ title = resourceReference(id = R.string.warning_demo_mode_title),
+ message = resourceReference(id = R.string.warning_demo_mode_message),
+ firstAction = EventMessageAction(
+ title = resourceReference(id = R.string.common_ok),
+ onClick = onConfirmClick,
+ ),
+ )
+
+ private fun transactionErrorDialog(
+ cause: String? = null,
+ causeTextReference: TextReference? = null,
+ code: String,
+ onConfirmClick: () -> Unit,
+ ): DialogMessage = DialogMessage(
+ title = resourceReference(id = R.string.send_alert_transaction_failed_title),
+ message = resourceReference(
+ id = R.string.send_alert_transaction_failed_text,
+ formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
+ ),
+ firstAction = EventMessageAction(
+ title = resourceReference(id = R.string.common_support),
+ onClick = onConfirmClick,
+ ),
+ )
+}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt
deleted file mode 100644
index e63ccb0229..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.tangem.common.ui.alerts.models
-
-import com.tangem.common.ui.R
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
-
-data class AlertDemoModeUM(
- override val onConfirmClick: () -> Unit,
-) : AlertUM {
- override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
- override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
- override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt
deleted file mode 100644
index 6d9cea587a..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.tangem.common.ui.alerts.models
-
-import com.tangem.common.ui.R
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.wrappedList
-
-data class AlertTransactionErrorUM(
- val code: String,
- val cause: String?,
- val causeTextReference: TextReference? = null,
- override val onConfirmClick: () -> Unit,
-) : AlertUM {
- override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
- override val message: TextReference = resourceReference(
- id = R.string.send_alert_transaction_failed_text,
- formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
- )
- override val confirmButtonText: TextReference =
- resourceReference(id = R.string.common_support)
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt
deleted file mode 100644
index 1cf955bebe..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.tangem.common.ui.alerts.models
-
-import androidx.compose.runtime.Immutable
-import com.tangem.core.ui.extensions.TextReference
-
-@Immutable
-interface AlertUM {
- val title: TextReference?
- val message: TextReference
- val confirmButtonText: TextReference
- val onConfirmClick: (() -> Unit)?
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt
index 4b4ca2eb11..7af3019d50 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt
@@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
+@Deprecated("Use GiveApprovalComponent")
@Composable
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
var isPermissionAlertShow by remember { mutableStateOf(false) }
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt
deleted file mode 100644
index 15bac87bdc..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.tangem.common.ui.news
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.tangem.core.ui.components.label.Label
-import com.tangem.core.ui.components.label.entity.LabelUM
-import com.tangem.core.ui.res.TangemTheme
-import kotlinx.collections.immutable.ImmutableList
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-fun ArticleHeader(
- title: String,
- createdAt: String,
- score: Float,
- tags: ImmutableList,
- modifier: Modifier = Modifier,
-) {
- Column(modifier = modifier) {
- ArticleInfo(
- score = score,
- createdAt = createdAt,
- )
-
- Spacer(modifier = Modifier.height(12.dp))
-
- Text(
- text = title,
- style = TangemTheme.typography.h2,
- color = TangemTheme.colors.text.primary1,
- )
-
- if (tags.isNotEmpty()) {
- Spacer(modifier = Modifier.height(20.dp))
- FlowRow(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- tags.forEach { tag ->
- Label(
- state = tag,
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt
deleted file mode 100644
index 300d41f104..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.tangem.common.ui.news
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.tangem.core.ui.components.RectangleShimmer
-import com.tangem.core.ui.components.SpacerH
-import com.tangem.core.ui.components.block.BlockCard
-import com.tangem.core.ui.components.block.TangemBlockCardColors
-import com.tangem.core.ui.res.TangemTheme
-
-@Composable
-fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
- BlockCard(
- modifier = modifier,
- colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 24.dp, horizontal = 16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
- SpacerH(12.dp)
- RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
- SpacerH(6.dp)
- RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
- SpacerH(14.dp)
- RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
- SpacerH(32.dp)
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
- RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
- RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
- RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
- }
- }
- }
-}
-
-@Composable
-fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
- BlockCard(
- modifier = modifier,
- colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
- ) {
- Column(modifier = Modifier.padding(12.dp)) {
- RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
- SpacerH(12.dp)
- RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
- SpacerH(6.dp)
- RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
- SpacerH(6.dp)
- RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
- SpacerH(16.dp)
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
- RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
- RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt
index 7f8ec96750..161788fcb3 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt
@@ -1,17 +1,35 @@
package com.tangem.common.ui.notifications
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+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 com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
+import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.message.TangemMessage
+import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
fun LazyListScope.notifications(
notifications: ImmutableList,
@@ -106,8 +124,107 @@ fun LazyListScope.notifications(
contentColor = contentColor,
modifier = modifier
.padding(top = topPadding)
- .animateItem(),
+ .animateItem(null, null, null),
)
},
)
-}
\ No newline at end of file
+}
+
+/**
+ * Displays a list of notifications in a stacked manner using a HorizontalPager.
+ * If there are multiple notifications, a PagerIndicator is shown below the notifications.
+ *
+ * @param notifications List of TangemMessageUM objects to be displayed.
+ * @param containerColor Color to be used for the background of the notifications.
+ * @param modifier Optional Modifier for the notifications.
+ */
+fun LazyListScope.notificationsCarousel(
+ notifications: ImmutableList?,
+ containerColor: Color,
+ modifier: Modifier = Modifier,
+) {
+ item {
+ if (!notifications.isNullOrEmpty()) {
+ val notificationsPagerState = rememberPagerState(
+ pageCount = { notifications.size },
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = TangemTheme.dimens2.x2),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
+ ) {
+ HorizontalPager(
+ state = notificationsPagerState,
+ modifier = Modifier
+ .fillMaxSize()
+ .animateItem(null, null, null),
+ ) { page ->
+ TangemMessage(
+ messageUM = notifications[page],
+ contentColor = containerColor,
+ modifier = modifier,
+ )
+ }
+ if (notifications.size > 1) {
+ TangemPagerIndicator(
+ pagerState = notificationsPagerState,
+ )
+ }
+ }
+ }
+ }
+}
+
+// region Preview
+@Composable
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun StackedNotifications_Preview(
+ @PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList,
+) {
+ TangemThemePreviewRedesign {
+ val contentColor = TangemTheme.colors2.surface.level1
+ LazyColumn(
+ modifier = Modifier
+ .background(contentColor)
+ .padding(16.dp),
+ ) {
+ notificationsCarousel(
+ notifications = params,
+ containerColor = contentColor,
+ )
+ }
+ }
+}
+
+private class StackedNotificationsPreviewProvider : PreviewParameterProvider> {
+ override val values: Sequence>
+ get() = sequenceOf(
+ persistentListOf(
+ TangemMessageUM(
+ id = "0",
+ title = stringReference("First notification"),
+ subtitle = stringReference("This is the first notification"),
+ messageEffect = TangemMessageEffect.Magic,
+ ),
+ ),
+ persistentListOf(
+ TangemMessageUM(
+ id = "0",
+ title = stringReference("First notification"),
+ subtitle = stringReference("This is the first notification"),
+ messageEffect = TangemMessageEffect.Magic,
+ ),
+ TangemMessageUM(
+ id = "1",
+ title = stringReference("Second notification"),
+ subtitle = stringReference("This is the second notification"),
+ messageEffect = TangemMessageEffect.Card,
+ ),
+ ),
+ )
+}
+// endregion
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt
new file mode 100644
index 0000000000..0a7afcbb8d
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt
@@ -0,0 +1,55 @@
+package com.tangem.common.ui.userwallet.converter
+
+import androidx.compose.ui.graphics.Color
+import androidx.core.graphics.toColorInt
+import com.tangem.core.ui.ds.image.DeviceIconUM
+import com.tangem.domain.models.wallet.UserWalletIcon
+import com.tangem.utils.converter.Converter
+import javax.inject.Inject
+
+/**
+ * Converter for mapping [UserWalletIcon] to [DeviceIconUM],
+ * which is used for displaying the wallet icon in the UI.
+ */
+class WalletIconUMConverter @Inject constructor() : Converter {
+
+ override fun convert(value: UserWalletIcon): DeviceIconUM = with(value) {
+ fun String.parseHexColor(): Color = try {
+ Color(this.toColorInt())
+ } catch (_: IllegalArgumentException) {
+ Color.Unspecified
+ }
+
+ return when (this) {
+ UserWalletIcon.Hot -> DeviceIconUM.Mobile
+ is UserWalletIcon.Stub ->
+ DeviceIconUM.Stub(cardsCount = this.cardsCount)
+ is UserWalletIcon.Default -> if (isRing) {
+ DeviceIconUM.Ring(
+ mainColor = Color.Unspecified,
+ cardColor = Color.Unspecified,
+ secondCardColor = if (cardsCount > 2) Color.Unspecified else null,
+ )
+ } else {
+ DeviceIconUM.Card(
+ mainColor = Color.Unspecified,
+ secondColor = if (cardsCount > 1) Color.Unspecified else null,
+ thirdColor = if (cardsCount > 2) Color.Unspecified else null,
+ )
+ }
+ is UserWalletIcon.Colored -> if (isRing) {
+ DeviceIconUM.Ring(
+ mainColor = mainColor.parseHexColor(),
+ cardColor = secondColor?.parseHexColor(),
+ secondCardColor = thirdColor?.parseHexColor(),
+ )
+ } else {
+ DeviceIconUM.Card(
+ mainColor = mainColor.parseHexColor(),
+ secondColor = secondColor?.parseHexColor(),
+ thirdColor = thirdColor?.parseHexColor(),
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt
index 3934f4ba5c..30c1e3b07e 100644
--- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt
+++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt
@@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig
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.EnvironmentConfigStorage
-import com.tangem.utils.Provider
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@@ -24,7 +23,7 @@ internal object ABTestsManagerModule {
@Singleton
fun provideABTestsManager(
application: Application,
- environmentConfigStorage: EnvironmentConfigStorage,
+ environmentConfig: EnvironmentConfig,
dispatchers: CoroutineDispatcherProvider,
): ABTestsManager {
return if (BuildConfig.AB_TESTS_ENABLED) {
@@ -32,7 +31,7 @@ internal object ABTestsManagerModule {
} else {
AmplitudeABTestsManager(
application = application,
- apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
+ apiKey = environmentConfig.amplitudeApiKey,
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt
index d2b6dbf52c..2307363d90 100644
--- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt
+++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt
@@ -7,14 +7,13 @@ 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 com.tangem.utils.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class AmplitudeABTestsManager(
val application: Application,
- val apiKeyProvider: Provider,
+ val apiKey: String,
val scope: CoroutineScope,
) : ABTestsManager {
@@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager(
client = Experiment.initializeWithAmplitudeAnalytics(
application = application,
- apiKey = apiKeyProvider(),
+ apiKey = apiKey,
config = ExperimentConfig
.builder()
.automaticFetchOnAmplitudeIdentityChange(true)
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
index ae0d6a4253..946ad653c4 100644
--- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
@@ -98,6 +98,7 @@ sealed class AnalyticsParam {
data object NewsLink : ScreensSources("News Link")
data object NewsPage : ScreensSources("News Page")
data object Portfolio : ScreensSources("Portfolio")
+ data object Staking : ScreensSources("Staking")
}
sealed class TxSentFrom(val value: String) {
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt
new file mode 100644
index 0000000000..e0271457e7
--- /dev/null
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt
@@ -0,0 +1,17 @@
+package com.tangem.core.analytics.models.event
+
+import com.tangem.core.analytics.models.AnalyticsEvent
+
+/**
+ * Offramp (withdraw/sell) analytics events
+ */
+sealed class OfframpAnalyticsEvent(
+ event: String,
+ params: Map = emptyMap(),
+) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) {
+
+ /**
+ * Withdraw screen opened event
+ */
+ data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened")
+}
\ No newline at end of file
diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
index c4ccee2282..7399fee331 100644
--- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
@@ -7,14 +7,7 @@
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
},
- {
- "name": "STAKING_TON_ENABLED",
- "version": "5.28.0"
- },
- {
- "name": "STAKING_CARDANO_ENABLED",
- "version": "5.31.1"
- },
+
{
"name": "STAKING_ETH_ENABLED",
"version": "undefined"
@@ -31,52 +24,40 @@
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
"version": "5.32.0"
},
- {
- "name": "TANGEM_PAY_ENABLED",
- "version": "5.31.0"
- },
- {
- "name": "YIELD_SUPPLY_FEATURE_ENABLED",
- "version": "5.30.0"
- },
- {
- "name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
- "version": "5.33.0"
- },
- {
- "name": "NEW_ONRAMP_MAIN_ENABLED",
- "version": "5.31.0"
- },
- {
- "name": "ACCOUNTS_FEATURE_ENABLED",
- "version": "5.33.0"
- },
- {
- "name": "FEED_ENABLED",
- "version": "5.33.0"
- },
{
"name": "APP_REDESIGN_ENABLED",
"version": "undefined"
},
- {
- "name": "GASLESS_TRANSACTIONS_ENABLED",
- "version": "5.33.0"
- },
{
"name": "SWAP_MARKET_LIST_ENABLED",
"version": "5.34"
},
{
"name": "EARN_BLOCK_ENABLED",
- "version": "undefined"
+ "version": "5.35"
},
{
"name": "HOLD_TO_CONFIRM_BUTTON_ENABLED",
- "version": "undefined"
+ "version": "5.35"
},
{
"name": "WALLET_REORDER_FEATURE_ENABLED",
"version": "5.34"
+ },
+ {
+ "name": "GASLESS_APPROVAL_ENABLED",
+ "version": "undefined"
+ },
+ {
+ "name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED",
+ "version": "undefined"
+ },
+ {
+ "name": "MULTI_ADDRESS_UTXO_ENABLED",
+ "version": "undefined"
+ },
+ {
+ "name": "CUSTOMER_IO_ENABLED",
+ "version": "5.35"
}
]
diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts
index 8df1d5a9f9..353cb3f5ab 100644
--- a/core/datasource/build.gradle.kts
+++ b/core/datasource/build.gradle.kts
@@ -1,4 +1,6 @@
+import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
+import com.tangem.plugin.configuration.model.BuildType
plugins {
alias(deps.plugins.android.library)
@@ -10,6 +12,23 @@ plugins {
id("configuration")
}
+abstract class GenerateEnvironmentConfigTask : DefaultTask() {
+
+ @get:InputFile
+ abstract val configFile: RegularFileProperty
+
+ @get:OutputDirectory
+ abstract val outputDir: DirectoryProperty
+
+ @TaskAction
+ fun generate() {
+ val input = configFile.get().asFile
+ require(input.exists()) { "Config file not found: ${input.absolutePath}" }
+ logger.lifecycle("Generating EnvironmentConfig from ${input.name}")
+ EnvironmentConfigGenerator.generate(input, outputDir.get().asFile)
+ }
+}
+
android {
namespace = "com.tangem.datasource"
@@ -18,6 +37,27 @@ android {
}
}
+androidComponents {
+ onVariants { variant ->
+ val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug
+ val configFile = rootProject.file(
+ "app/src/main/assets/tangem-app-config/config_${buildType.environment}.json",
+ )
+
+ val taskProvider = tasks.register(
+ "generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}",
+ ) {
+ this.configFile.set(configFile)
+ outputDir.set(layout.buildDirectory.dir("generated/source/environment-config/${variant.name}"))
+ doFirst {
+ logger.lifecycle("[Environment config] Running: ${this.name}")
+ }
+ }
+
+ variant.sources.java?.addGeneratedSourceDirectory(taskProvider, GenerateEnvironmentConfigTask::outputDir)
+ }
+}
+
tasks.withType().configureEach {
useJUnitPlatform()
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt
index f05797c816..7fcda27661 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt
@@ -1,11 +1,10 @@
package com.tangem.datasource.api.common.config
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
-import kotlinx.coroutines.flow.first
internal class BlockAid(
- private val configStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
@@ -21,9 +20,7 @@ internal class BlockAid(
put(
key = "X-API-KEY",
value = ProviderSuspend {
- requireNotNull(
- configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey,
- )
+ requireNotNull(environmentConfig.blockAidApiKey)
},
)
put("accept", ProviderSuspend { "application/json" })
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt
index 4104dae025..b9923936b3 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt
@@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.ProviderSuspend
@@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider
/**
* Express [ApiConfig]
*
- * @property environmentConfigStorage environment config storage
+ * @property environmentConfig environment config
* @property expressAuthProvider express auth provider
* @property appVersionProvider app version provider
* @property appInfoProvider app info provider
*/
internal class Express(
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
private val expressAuthProvider: ExpressAuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
@@ -100,9 +100,9 @@ internal class Express(
private fun getApiKey(isProd: Boolean): String {
return if (isProd) {
- environmentConfigStorage.getConfigSync().express
+ environmentConfig.express
} else {
- environmentConfigStorage.getConfigSync().devExpress
+ environmentConfig.devExpress
}
?.apiKey
?: error("No express config provided")
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
index b494cbc70c..5c20eca2cf 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
@@ -1,13 +1,13 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal sealed class TangemPay(
+ private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
- private val environmentConfigStorage: EnvironmentConfigStorage,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
@@ -61,8 +61,8 @@ internal sealed class TangemPay(
return when (apiEnvironment) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
- -> environmentConfigStorage.getConfigSync().bffStaticTokenDev
- ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken
+ -> environmentConfig.bffStaticTokenDev
+ ApiEnvironment.PROD -> environmentConfig.bffStaticToken
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
ApiEnvironment.DEV_2,
@@ -72,9 +72,9 @@ internal sealed class TangemPay(
}
class Bff(
+ environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
- environmentConfigStorage: EnvironmentConfigStorage,
- ) : TangemPay(appVersionProvider, environmentConfigStorage) {
+ ) : TangemPay(environmentConfig, appVersionProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
@@ -90,9 +90,9 @@ internal sealed class TangemPay(
}
class Auth(
+ environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
- environmentConfigStorage: EnvironmentConfigStorage,
- ) : TangemPay(appVersionProvider, environmentConfigStorage) {
+ ) : TangemPay(environmentConfig, appVersionProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt
index b5e7ea38c6..ed04143824 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt
@@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
@@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider
/** YieldSupply [ApiConfig] */
internal class YieldSupply(
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
@@ -78,8 +78,8 @@ internal class YieldSupply(
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
- -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
- ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
+ -> environmentConfig.yieldModuleApiKeyDev
+ ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey
} ?: error("No tangem tech api config provided")
}
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt
index 73725d5782..b293b1e3aa 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt
@@ -4,6 +4,11 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
-data class OrderRequest(
- @Json(name = "wallet_address") val walletAddress: String,
-)
\ No newline at end of file
+data class OrderRequest(@Json(name = "data") val data: Data) {
+ @JsonClass(generateAdapter = true)
+ data class Data(
+ @Json(name = "customer_wallet_address") val customerWalletAddress: String,
+ @Json(name = "specification_name") val specificationName: String = "SP_000004",
+ @Json(name = "type") val type: String = "CARD_ISSUE_VIRTUAL_RAIN_KYC",
+ )
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
index 69405843f7..38e8982664 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
@@ -73,7 +73,7 @@ data class CustomerMeResponse(
@JsonClass(generateAdapter = true)
data class PaymentAccount(
@Json(name = "id") val id: String,
- @Json(name = "address") val address: String,
+ @Json(name = "address") val address: String?,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt
index 48cc8fc4fa..e8ce00ce4f 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt
@@ -12,7 +12,7 @@ data class OrderResponse(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String?,
@Json(name = "type") val type: String?,
- @Json(name = "status") val status: String,
+ @Json(name = "status") val status: Status,
@Json(name = "step") val step: String?,
@Json(name = "data") val data: Data,
@Json(name = "step_change_code") val stepChangeCode: Int?,
@@ -29,5 +29,20 @@ data class OrderResponse(
@Json(name = "payment_account_id") val paymentAccountId: String?,
@Json(name = "transaction_hash") val transactionHash: String?,
)
+
+ @JsonClass(generateAdapter = false)
+ enum class Status {
+ @Json(name = "NEW")
+ NEW,
+
+ @Json(name = "PROCESSING")
+ PROCESSING,
+
+ @Json(name = "COMPLETED")
+ COMPLETED,
+
+ @Json(name = "CANCELED")
+ CANCELED,
+ }
}
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt
index 6938d499d7..40ffd06873 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt
@@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
internal class Sha256SignatureVerifier(
- private val environmentConfigStorage: EnvironmentConfigStorage,
+ private val environmentConfig: EnvironmentConfig,
private val apiConfigsManager: ApiConfigsManager,
) : DataSignatureVerifier {
@@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier(
private fun getPubKey(): String? {
val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express)
return when (expressConfig.environment) {
- ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey
- else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey
+ ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey
+ else -> environmentConfig.devExpress?.signVerifierPublicKey
}
}
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
index 20d4c632d5..e8db8ec219 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
@@ -2,7 +2,7 @@ package com.tangem.datasource.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
@@ -21,13 +21,13 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideExpressConfig(
- environmentConfigStorage: EnvironmentConfigStorage,
+ environmentConfig: EnvironmentConfig,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig {
return Express(
- environmentConfigStorage = environmentConfigStorage,
+ environmentConfig = environmentConfig,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
@@ -73,12 +73,12 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideYieldSupplyConfig(
- environmentConfigStorage: EnvironmentConfigStorage,
+ environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = YieldSupply(
- environmentConfigStorage = environmentConfigStorage,
+ environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
@@ -87,21 +87,21 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemPayBffConfig(
+ environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
- environmentConfigStorage: EnvironmentConfigStorage,
- ): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage)
+ ): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider)
@Provides
@IntoSet
fun provideTangemPayAuthConfig(
+ environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
- environmentConfigStorage: EnvironmentConfigStorage,
- ): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage)
+ ): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider)
@Provides
@IntoSet
- fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
- return BlockAid(environmentConfigStorage)
+ fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig {
+ return BlockAid(environmentConfig)
}
@Provides
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt
index c1e4a4f7dc..de63c7bbcd 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt
@@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM
+import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.*
import dagger.Module
@@ -45,6 +46,15 @@ class MoshiModule {
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
)
+ .add(
+ NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java)
+ .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created")
+ .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status")
+ .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card")
+ .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked")
+ .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance")
+ .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"),
+ )
.add(
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt
index 82d2ca69da..c04614c1f7 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt
@@ -3,7 +3,7 @@ package com.tangem.datasource.di
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.crypto.Sha256SignatureVerifier
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -17,9 +17,9 @@ internal object SecurityModule {
@Provides
@Singleton
fun provideDataSignatureVerifier(
- environmentConfigStorage: EnvironmentConfigStorage,
+ environmentConfig: EnvironmentConfig,
apiConfigsManager: ApiConfigsManager,
): DataSignatureVerifier {
- return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager)
+ return Sha256SignatureVerifier(environmentConfig, apiConfigsManager)
}
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt
index 0ed4457201..c8052af564 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt
@@ -1,9 +1,8 @@
package com.tangem.datasource.di.local.config
import com.tangem.datasource.asset.loader.AssetLoader
-import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
+import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
@@ -23,11 +22,8 @@ internal object ConfigModule {
@Provides
@Singleton
- fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage {
- return DefaultEnvironmentConfigStorage(
- assetLoader = assetLoader,
- environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()),
- )
+ fun provideEnvironmentConfig(): EnvironmentConfig {
+ return GeneratedEnvironmentConfigConverter.convert()
}
@Provides
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt
deleted file mode 100644
index 1ad363973d..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.tangem.datasource.local.config.environment
-
-import com.tangem.datasource.BuildConfig
-import com.tangem.datasource.asset.loader.AssetLoader
-import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter
-import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
-import com.tangem.datasource.local.datastore.RuntimeStateStore
-import kotlinx.coroutines.flow.Flow
-import timber.log.Timber
-
-/**
- * Default implementation for storing [EnvironmentConfig]
- *
- * @property assetLoader asset loader
- * @property environmentConfigStore config store
- */
-internal class DefaultEnvironmentConfigStorage(
- private val assetLoader: AssetLoader,
- private val environmentConfigStore: RuntimeStateStore,
-) : EnvironmentConfigStorage {
-
- override suspend fun initialize(): EnvironmentConfig {
- val environmentConfigModel = assetLoader.load(fileName = CONFIG_FILE_NAME)
- ?: return environmentConfigStore.get().value
-
- val config = EnvironmentConfigConverter.convert(value = environmentConfigModel)
- environmentConfigStore.store(value = config)
-
- Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully")
-
- return config
- }
-
- override fun getConfig(): Flow = environmentConfigStore.get()
-
- override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value
-
- private companion object {
- const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}"
- }
-}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt
index 22545b5329..21e234d6f5 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt
@@ -28,4 +28,6 @@ data class EnvironmentConfig(
val bffStaticTokenDev: String? = null,
val gaslessTxApiKeyDev: String? = null,
val gaslessTxApiKey: String? = null,
+ val customerIoCdpApiKey: String? = null,
+ val surveySparrowToken: String? = null,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt
deleted file mode 100644
index a46edc5d9a..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.tangem.datasource.local.config.environment
-
-import kotlinx.coroutines.flow.Flow
-
-/**
- * Storage for [EnvironmentConfig]
- *
-[REDACTED_AUTHOR]
- */
-interface EnvironmentConfigStorage {
-
- /** Initialize and return [EnvironmentConfig] */
- suspend fun initialize(): EnvironmentConfig
-
- /** Get [EnvironmentConfig] as [Flow] */
- fun getConfig(): Flow
-
- /** Get [EnvironmentConfig] synchronously */
- fun getConfigSync(): EnvironmentConfig
-}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt
new file mode 100644
index 0000000000..28644f5c6b
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt
@@ -0,0 +1,183 @@
+package com.tangem.datasource.local.config.environment.converter
+
+import com.tangem.blockchain.common.*
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey
+import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey
+import com.tangem.datasource.local.config.environment.models.ExpressModel
+import com.tangem.datasource.local.config.environment.models.P2PKeys
+
+/**
+ * Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig]
+ *
+ * This converter maps the auto-generated config (from JSON) to the domain model.
+ * The generated config has nested objects that mirror the JSON structure.
+ */
+internal object GeneratedEnvironmentConfigConverter {
+
+ fun convert(): EnvironmentConfig {
+ return EnvironmentConfig(
+ moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey,
+ moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey,
+ mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId,
+ mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret,
+ blockchainSdkConfig = createBlockchainSdkConfig(),
+ amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey,
+ appsFlyerApiKey = AppsFlyer.appsFlyerDevKey,
+ appsAppId = AppsFlyer.appsFlyerAppID,
+ walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId,
+ express = createExpressModel(
+ apiKey = Express.apiKey,
+ signVerifierPublicKey = Express.signVerifierPublicKey,
+ ),
+ devExpress = createExpressModel(
+ apiKey = DevExpress.apiKey,
+ signVerifierPublicKey = DevExpress.signVerifierPublicKey,
+ ),
+ stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey,
+ p2pApiKey = createP2PKeys(),
+ blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey,
+ tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey,
+ tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev,
+ tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage,
+ yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey,
+ yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev,
+ bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken,
+ bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev,
+ gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev,
+ gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey,
+ customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.androidApiKey,
+ surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey,
+ )
+ }
+
+ private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? {
+ return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) {
+ ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey)
+ } else {
+ null
+ }
+ }
+
+ private fun createP2PKeys(): P2PKeys? {
+ val mainnet = P2pApiKey.mainnet
+ val hoodi = P2pApiKey.hoodi
+ return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) {
+ P2PKeys(mainnet = mainnet, hoodi = hoodi)
+ } else {
+ null
+ }
+ }
+
+ private fun createBlockchainSdkConfig(): BlockchainSdkConfig {
+ return BlockchainSdkConfig(
+ blockchairCredentials = BlockchairCredentials(
+ apiKey = GeneratedEnvironmentConfig.blockchairApiKeys,
+ authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken,
+ ),
+ blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(),
+ quickNodeSolanaCredentials = QuickNodeCredentials(
+ apiKey = GeneratedEnvironmentConfig.quiknodeApiKey,
+ subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain,
+ ),
+ quickNodeBscCredentials = QuickNodeCredentials(
+ apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey,
+ subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain,
+ ),
+ quickNodePlasmaCredentials = QuickNodeCredentials(
+ apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey,
+ subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain,
+ ),
+ quickNodeMonadCredentials = QuickNodeCredentials(
+ apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey,
+ subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain,
+ ),
+ infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId,
+ tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey,
+ nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey),
+ getBlockCredentials = createGetBlockCredentials(),
+ kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl,
+ tonCenterCredentials = TonCenterCredentials(
+ mainnetApiKey = TonCenterApiKey.mainnet,
+ testnetApiKey = TonCenterApiKey.testnet,
+ ),
+ chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey,
+ chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey,
+ hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey,
+ polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey,
+ bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey,
+ bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey,
+ dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey,
+ koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey,
+ alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey,
+ moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey,
+ etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey,
+ blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey,
+ tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey,
+ )
+ }
+
+ private fun createGetBlockCredentials(): GetBlockCredentials {
+ return GetBlockCredentials(
+ xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc),
+ cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta),
+ avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc),
+ eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc),
+ etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc),
+ fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc),
+ rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc),
+ bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc),
+ polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc),
+ gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc),
+ cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc),
+ solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc),
+ ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc),
+ tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest),
+ cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest),
+ near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc),
+ aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest),
+ dogecoin = GetBlockAccessToken(
+ jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc,
+ blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest,
+ ),
+ litecoin = GetBlockAccessToken(
+ jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc,
+ blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest,
+ ),
+ dash = GetBlockAccessToken(
+ jsonRpc = GetBlockAccessTokens.Dash.jsonRpc,
+ blockBookRest = GetBlockAccessTokens.Dash.blockBookRest,
+ ),
+ bitcoin = GetBlockAccessToken(
+ jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc,
+ blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest,
+ ),
+ algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest),
+ zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc),
+ polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc),
+ base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc),
+ blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc),
+ filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc),
+ arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc),
+ bitcoinCash = GetBlockAccessToken(
+ jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc,
+ blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest,
+ ),
+ kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc),
+ moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc),
+ optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc),
+ polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc),
+ shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc),
+ sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc),
+ telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc),
+ tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest),
+ monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest),
+ stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest),
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
index c70f061ee1..6762fd15f3 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
@@ -79,10 +79,6 @@ object PreferencesKeys {
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
- val MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
- booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked")
- }
-
val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") }
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt
new file mode 100644
index 0000000000..589fb4d915
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt
@@ -0,0 +1,53 @@
+@file:Suppress("BooleanPropertyNaming")
+package com.tangem.datasource.local.visa.entity
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.tangem.domain.models.kyc.KycStatus
+import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
+import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
+import java.math.BigDecimal
+
+/**
+ * Payment account status for storage in the local cache.
+ *
+ * @see [com.tangem.domain.pay.PaymentAccountStatus]
+ */
+@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
+sealed interface PaymentAccountStatusDM {
+
+ @NameLabel("not_created")
+ data class NotCreated(
+ @Json(name = "not_created") val marker: Boolean = true,
+ ) : PaymentAccountStatusDM
+
+ @NameLabel("kyc_status")
+ data class UnderReview(
+ @Json(name = "kyc_status") val kycStatus: KycStatus,
+ ) : PaymentAccountStatusDM
+
+ @NameLabel("issuing_card")
+ data class IssuingCard(
+ @Json(name = "issuing_card") val marker: Boolean = true,
+ ) : PaymentAccountStatusDM
+
+ @NameLabel("locked")
+ data class Locked(
+ @Json(name = "locked") val marker: Boolean = true,
+ ) : PaymentAccountStatusDM
+
+ @NameLabel("balance")
+ data class Loaded(
+ @Json(name = "card_id") val cardId: String,
+ @Json(name = "last_four_digits") val lastFourDigits: String,
+ @Json(name = "balance") val balance: BigDecimal,
+ @Json(name = "currency_code") val currencyCode: String,
+ @Json(name = "deposit_address") val depositAddress: String?,
+ @Json(name = "is_pin_set") val isPinSet: Boolean,
+ ) : PaymentAccountStatusDM
+
+ @NameLabel("card_issue_failed")
+ data class CardIssueFailed(
+ @Json(name = "card_issue_failed") val marker: Boolean = true,
+ ) : PaymentAccountStatusDM
+}
\ No newline at end of file
diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt
index 2e0ccb145b..5650f72e16 100644
--- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt
+++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt
@@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.AuthProvider
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import io.mockk.clearMocks
import io.mockk.every
@@ -19,6 +20,7 @@ class ApiConfigTest {
private val appAuthProvider = mockk()
private val apiKeyProvider = mockk>()
+ private val environmentConfig = mockk()
@BeforeEach
fun setup() {
@@ -47,7 +49,7 @@ class ApiConfigTest {
when (it) {
ApiConfig.ID.Express -> {
Express(
- environmentConfigStorage = mockk(),
+ environmentConfig = environmentConfig,
expressAuthProvider = mockk(),
appVersionProvider = mockk(),
appInfoProvider = mockk(),
@@ -55,7 +57,7 @@ class ApiConfigTest {
}
ApiConfig.ID.YieldSupply -> {
YieldSupply(
- environmentConfigStorage = mockk(),
+ environmentConfig = environmentConfig,
appVersionProvider = mockk(),
authProvider = appAuthProvider,
appInfoProvider = mockk(),
@@ -70,14 +72,14 @@ class ApiConfigTest {
}
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay.Bff(
+ environmentConfig = environmentConfig,
appVersionProvider = mockk(),
- environmentConfigStorage = mockk()
)
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
+ environmentConfig = environmentConfig,
appVersionProvider = mockk(),
- environmentConfigStorage = mockk()
)
- ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
+ ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
ApiConfig.ID.News -> News(
diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt
deleted file mode 100644
index 530d4de06e..0000000000
--- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.tangem.datasource.api.common.config.managers
-
-import com.tangem.datasource.local.config.environment.EnvironmentConfig
-import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
-import com.tangem.datasource.local.config.environment.models.ExpressModel
-import kotlinx.coroutines.flow.flowOf
-
-/**
- * Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest]
- *
-[REDACTED_AUTHOR]
- */
-internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
-
- private val environmentConfig = EnvironmentConfig(
- express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"),
- devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"),
- blockAidApiKey = BLOCK_AID_API_KEY,
- tangemApiKey = TANGEM_API_KEY,
- tangemApiKeyDev = TANGEM_API_KEY_DEV,
- bffStaticToken = TANGEM_PAY_BFF_KEY,
- bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
- tangemApiKeyStage = TANGEM_API_KEY_STAGE,
- yieldModuleApiKey = YIELD_MODULE_KEY,
- yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
- )
-
- override suspend fun initialize() = environmentConfig
- override fun getConfig() = flowOf(environmentConfig)
- override fun getConfigSync() = environmentConfig
-
- companion object {
- const val EXPRESS_API_KEY = "express_api_key"
- const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
- const val BLOCK_AID_API_KEY = "block_aid_api_key"
- const val TANGEM_API_KEY = "tangem_api_key"
- const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
- const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key"
- const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
- const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
- const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
- const val YIELD_MODULE_KEY = "yield_module_api_key"
- const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
- }
-}
\ No newline at end of file
diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt
index 76572d804f..7b324258f2 100644
--- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt
+++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt
@@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
-import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
-import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
-import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY
-import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV
+import com.tangem.datasource.local.config.environment.EnvironmentConfig
+import com.tangem.datasource.local.config.environment.models.ExpressModel
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
@@ -39,7 +37,7 @@ import java.util.TimeZone
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ProdApiConfigsManagerTest {
- private val environmentConfigStorage = MockEnvironmentConfigStorage()
+ private val environmentConfig = createMockEnvironmentConfig()
private val appVersionProvider = mockk()
private val expressAuthProvider = mockk()
private val stakeKitAuthProvider = mockk()
@@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest {
when (it) {
ApiConfig.ID.Express -> {
Express(
- environmentConfigStorage = environmentConfigStorage,
+ environmentConfig = environmentConfig,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
@@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest {
}
ApiConfig.ID.YieldSupply -> {
YieldSupply(
- environmentConfigStorage = environmentConfigStorage,
+ environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,
@@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest {
}
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay.Bff(
+ environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
- environmentConfigStorage = environmentConfigStorage,
)
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
+ environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
- environmentConfigStorage = environmentConfigStorage,
)
- ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
+ ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
ApiConfig.ID.News -> News(
@@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest {
headers = mapOf(
"api-key" to ProviderSuspend {
if (environment == ApiEnvironment.PROD) {
- MockEnvironmentConfigStorage.EXPRESS_API_KEY
+ EXPRESS_API_KEY
} else {
- MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
+ EXPRESS_DEV_API_KEY
}
},
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
@@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest {
environment = ApiEnvironment.PROD,
baseUrl = "https://yield.tangem.org/",
headers = mapOf(
- "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY },
+ "api-key" to ProviderSuspend { YIELD_MODULE_KEY },
"card_id" to ProviderSuspend { APP_CARD_ID },
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
"version" to ProviderSuspend { VERSION_NAME },
@@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest {
const val P2P_API_KEY = "p2p_api_key"
const val APP_CARD_ID = "app_card_id"
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
+
+ // Mock config values
+ const val TANGEM_API_KEY = "tangem_api_key"
+ const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
+ const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
+ const val BLOCK_AID_API_KEY = "block_aid_api_key"
+ const val EXPRESS_API_KEY = "express_api_key"
+ const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
+ const val YIELD_MODULE_KEY = "yield_module_key"
+
+ fun createMockEnvironmentConfig(): EnvironmentConfig {
+ return EnvironmentConfig(
+ moonPayApiKey = "moon_pay_api_key",
+ moonPayApiSecretKey = "moon_pay_secret_key",
+ mercuryoWidgetId = "mercuryo_widget_id",
+ mercuryoSecret = "mercuryo_secret",
+ blockchainSdkConfig = mockk(relaxed = true),
+ amplitudeApiKey = "amplitude_api_key",
+ appsFlyerApiKey = "appsflyer_api_key",
+ appsAppId = "apps_app_id",
+ walletConnectProjectId = "wallet_connect_project_id",
+ express = ExpressModel(
+ apiKey = EXPRESS_API_KEY,
+ signVerifierPublicKey = "express_public_key",
+ ),
+ devExpress = ExpressModel(
+ apiKey = EXPRESS_DEV_API_KEY,
+ signVerifierPublicKey = "express_dev_public_key",
+ ),
+ stakeKitApiKey = STAKE_KIT_API_KEY,
+ p2pApiKey = null,
+ blockAidApiKey = BLOCK_AID_API_KEY,
+ tangemApiKey = TANGEM_API_KEY,
+ tangemApiKeyDev = TANGEM_API_KEY,
+ tangemApiKeyStage = TANGEM_API_KEY,
+ yieldModuleApiKey = YIELD_MODULE_KEY,
+ yieldModuleApiKeyDev = YIELD_MODULE_KEY,
+ bffStaticToken = TANGEM_PAY_BFF_KEY_DEV,
+ bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
+ gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY,
+ gaslessTxApiKey = TANGEM_GASLESS_API_KEY,
+ )
+ }
}
}
\ No newline at end of file
diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml
index 4cc165f633..de8a0facb8 100644
--- a/core/res/src/main/res/values-de/strings.xml
+++ b/core/res/src/main/res/values-de/strings.xml
@@ -217,6 +217,7 @@
Hinzufügen
Zum Portfolio hinzufügen
Token hinzufügen
+ Token hinzufügen
Hinzugefügt
Vertragsadresse
Alle
@@ -399,6 +400,7 @@
Staking beenden
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.
Wert kopiert
+ Meine Wallet
Woche
mit
Ja
@@ -469,9 +471,12 @@
Sende Geld nur mit
Beste Gelegenheiten
Filter löschen
+ Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein.
Alle Netzwerke
Alle Arten
Filtern nach
+ Meine Netzwerke
+ Netzwerke
Meist verwendet
Keine Ergebnisse
Verdienen
@@ -826,6 +831,7 @@
Volumen
Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen
Token hinzufügen
+ Token hinzufügen
Steiger die Leistung Deiner Assets und ermögliche Dir gleichzeitig den sofortigen Zugriff. %s
Yield-Modus aktivieren
Du musst auf die folgende Version aktualisieren: %1$s um eine mobile Wallet zu erstellen
@@ -845,6 +851,7 @@
Verwandte Token
Verwandte Nachrichten
Auf dem Laufenden bleiben
+ Trend-Score
NFC ist auf deinem Gerät nicht verfügbar
Über NFT
NFT-Vermögenswert
@@ -963,12 +970,12 @@
Biometrische Daten
Lese mehr über die Seed-Phrase
-
+ - .
- Schreibe diese %d-Wörter in der unten angegebenen Reihenfolge auf und bewahre sie an einem sicheren und geheimen Ort auf.
Deine Seed-Phrase
-
+ - .
- %d Wörter
Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein
@@ -1101,9 +1108,9 @@
- für %d Wallet
- für %d Wallets
- Du bekommst ^^%1$s^^ für jede von einem Freund gekaufte Wallet auf deine %2$s Netzwerkadresse %3$s ^^30 Tage nach^^ dem
- Du
- Bekommt eine
+ 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
+ Du erhältst
+ Er erhält
beim Kauf einer Wallet auf tangem.com
%s Rabatt
Dein Freund
@@ -1222,6 +1229,7 @@
Begrenzung der Transaktionen
Optional
Bitte richte den QR-Code auf das Quadrat aus, um ihn zu scannen. Stell sicher, dass du die Netzwerkadresse %s scannen.
+ Ein variabler Zinssatz bedeutet, dass sich der endgültige Betrag, den Sie erhalten, je nach Marktbedingungen zwischen dem Beginn und dem Abschluss des Swaps geringfügig ändern kann.
Jüngste
Empfänger
Keine gültige Adresse
@@ -1431,6 +1439,7 @@
Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen.
Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche.
Neuer Swap-Anbieter verfügbar!
+ Suchen Sie etwas anderes?\n Versuchen Sie es mit der Suche oder erkunden Sie eine andere Kryptowährung!
Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist.
Nutzen Sie die Suche, um zu finden, was Sie benötigen.
Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen
diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml
index 0ec38e3506..4fde82b922 100644
--- a/core/res/src/main/res/values-es/strings.xml
+++ b/core/res/src/main/res/values-es/strings.xml
@@ -217,6 +217,7 @@
Agregar
Añadir al portafolio
Agregar token
+ Añada tokens
Agregado
Dirección
Todos
diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml
index 41913d6515..89c9435c9b 100644
--- a/core/res/src/main/res/values-fr/strings.xml
+++ b/core/res/src/main/res/values-fr/strings.xml
@@ -217,6 +217,7 @@
Ajouter
Ajouter au portfolio
Ajouter un jeton
+ Ajouter des jetons
Ajouté
Adresse
Tous
diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml
index 6b87995090..42b792db6e 100644
--- a/core/res/src/main/res/values-ja/strings.xml
+++ b/core/res/src/main/res/values-ja/strings.xml
@@ -214,6 +214,7 @@
追加
ポートフォリオに追加
トークンを追加
+ トークンの追加
追加済み
アドレス
すべて
@@ -319,6 +320,7 @@
NFT
いいえ
アドレスがありません
+ 該当する結果はありません
未追加
利用できません
今はしない
@@ -377,6 +379,7 @@
宛先
%sへ
今日
+ 送信するトークン
- %d トークン
@@ -388,6 +391,7 @@
わかりました
理解して続行
エラーが発生しました。もう一度お試しください。
+ ロック解除
アクセスできません
ステーキング解除
%1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。
@@ -692,6 +696,7 @@
Manaレベル
暗号資産および取引の追跡を開始するには、トークンを追加してください
トークンの管理
+ QRコードをスキャンして送金するか、アプリに接続します。
すべてのネットワークにアクセスするには、カードをスキャンする必要があります。
カードまたはリングをスキャンする
2月%2$s - %3$sの期間、Changelly経由のスワップは%1$sのサービス手数料となります。
@@ -728,6 +733,8 @@
Tangemで稼ぐ
選択したネットワークのアドレスを生成するには、Tangemウォレットカードまたはリングをスキャンする必要があります。
トークンを追加するには、これをスワイプするか、検索バーをタップしてください。
+ 上にスワイプしてマーケットをチェック
+ 新たな有望銘柄を見つける
このセクションのデータは、次のネットワークから取得されています: %s
データを読み込めません…
データなし
@@ -788,6 +795,10 @@
ホルダー
特定の期間内のトークンホルダー数の変化
ホルダー
+ 日
+ 月
+ 週
+ 年
インサイト
リンク
流動性
@@ -815,9 +826,11 @@
総供給量
特定の暗号資産に存在しうるコインまたはトークンの最大数
総供給量
+ 24h
取引量(24時間)
過去24時間以内に取引された暗号資産の合計額。市場の活発さと流動性を示します。
取引量(24時間)
+ 合計 %s
取引量
これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します
トークンを追加
@@ -839,6 +852,7 @@
関連トークン
関連ニュース
最新情報を入手
+ トレンドスコア
お使いのデバイスではNFCが使用できません
NFTについて
NFTアセット
@@ -1181,6 +1195,9 @@
メモ: %s
無効なメモ
ネットワーク手数料のカバー
+
+ - %dトークンは、このアドレスには対応していません。
+
ナンス
各取引に固有の番号です。保留中の取引を再送信またはキャンセルするために使用します。
ナンスを入力してください...
@@ -1488,6 +1505,7 @@
メイン画面からKYCを非表示にする
資金を追加
入金オプション
+ Googleウォレットに追加
カード番号
PINを変更する
カードは支払いの準備が整いました。
@@ -1876,6 +1894,8 @@
サービスは一時的に利用できません
スワップするトークンの量は %s を超えないでください
スワップ金額は %s 以上である必要があります
+ このペアではスワップを利用できません。別のトークンを選択して、もう一度お試しください。
+ スワップ非対応のペアです
スワップの金額を変更してください
このカードは、サンプル品または偽造品である可能性があります
真正性チェックに失敗しました
diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..04ca28b1d4
--- /dev/null
+++ b/core/res/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,2197 @@
+
+
+ Sem um código de acesso, sua carteira não está segura.
+ Ignorar de qualquer forma
+ Código de acesso não configurado
+ Alterar código
+ Seu código de acesso desbloqueia e protege o acesso à sua carteira.
+ Use de qualquer forma
+ Este código de acesso pode ser facilmente adivinhado.
+ Insira o código de acesso
+ Código de acesso incorreto. Sua carteira digital será excluída após %s mais tentativas incorretas.
+ Código de acesso incorreto. O aplicativo será bloqueado em seguida. %s mais tentativas fracassadas
+ Código de acesso incorreto.\nPor favor, aguarde %s segundos e tente novamente.
+ Confirme seu código de acesso para continuar.
+ Digite novamente o código de acesso.
+ Defina um %sCódigo de acesso de -dígitos para desbloquear sua carteira.
+ Criar código de acesso
+ Código de acesso
+ Você não pode criar mais do que %1$s Contas. Arquive uma para adicionar uma nova.
+ Não é possível adicionar uma nova conta.
+ Conta arquivada
+ Contas arquivadas
+ Recuperar
+ Você está prestes a recuperar \"%1$s\".
+ Recuperar conta
+ Você atingiu o limite de 20 contas ativas. Arquive uma conta para recuperar esta.
+ Não consigo recuperar a conta.
+ Arquivado
+ Não foi possível arquivar a conta. Tente novamente mais tarde.
+ Esta conta participa do programa de indicações.
+ Esta conta não pode ser arquivada.
+ Não foi possível criar a conta. Tente novamente mais tarde.
+ Conta criada
+ Conta arquivada
+ Arquivo
+ Você está arquivando esta conta, mas poderá recuperá-la a qualquer momento.
+ Arquivando...
+ Conta
+ Não consigo editar a conta.
+ Conta salva
+ Conta para recompensas
+ Conta #%s — usado para derivação de endereços.
+ Adicionar conta
+ Salvar
+ Nome da conta
+ Já existe uma conta com esse nome. Por favor, escolha um nome diferente.
+ Nome da conta já em uso
+ Conta
+ Nova conta
+ Adicionar conta
+ Editar conta
+ Por favor, tente novamente mais tarde. Se o problema persistir, entre em contato com o suporte e nós o ajudaremos a resolvê-lo.
+ %1$s em %2$s
+ Conta principal
+ Você já ultrapassou o limite de %1$s contas ativas. Arquive uma para recuperar
+ Não consigo recuperar a conta.
+ Conta recuperada
+ Pressione e segure uma conta para reordená-las.
+ Continue editando
+ Descartar
+ Tem certeza de que deseja descartar a nova conta?
+ Tem certeza de que deseja descartar as edições?
+ Alterações não salvas
+ Alguns tokens personalizados serão movidos automaticamente de “%1$s\" para \"%2$s\"pois sua derivação pertence a essa explicação.\"
+ Alguns tokens personalizados serão movidos automaticamente.
+ Não consegue encontrar o seu token? Vá até a seção Mercado na página principal e adicione-o ao seu portfólio para compra.
+ Não consegue encontrar o seu token? Vá para a seção Mercado na página principal e adicione-o ao seu portfólio para venda.
+ Vender
+ Isso pode levar alguns segundos. Por favor, tente novamente mais tarde.
+ Os dados ainda não foram carregados.
+ A ação está indisponível no momento. Tente novamente mais tarde ou atualize os dados deslizando o dedo para baixo na tela.
+ A ação não está disponível.
+ Escolha o token
+ Não consegue encontrar o seu token? Vá para a seção Mercado na página principal e adicione-o ao seu portfólio para troca.
+ Não há tokens disponíveis para trocar pelo token selecionado. Por favor, escolha outro.
+ Nenhum par disponível
+ Para utilizar a funcionalidade de troca, sua carteira deve conter pelo menos 2 tokens.
+ Adicionar tokens
+ Você adicionou apenas 1 token à sua carteira. Para usar a função de troca, você precisa adicionar pelo menos 2 tokens.
+ Adicionar tokens
+ Escolha o token que deseja receber.
+ Escolha o token que deseja trocar.
+ Escolha a rede
+ Adicionar token personalizado
+ Gerenciar tokens
+ Enviar somente %1$s (%2$s) de %3$s A rede não está vinculada a este endereço. O uso de outros tokens e redes pode resultar na perda de fundos.
+ Padrão
+ Legado
+ Ocorreu um problema com a biometria. Tente redefinir a biometria do seu dispositivo ou entre em contato com o suporte.
+ Erro de autenticação
+ Como escanear
+ Solicitar suporte
+ Tente novamente
+ Este recurso está desativado no modo de demonstração.
+ Razão: %s
+ Não foi possível enviar uma transação.
+ A opção selecionada não é compatível com %1$s rede
+ Para ativar o %1$s Devido à criptografia do blockchain, você precisará redefinir a carteira para as configurações de fábrica. Por favor, retire seus fundos antes de fazer isso para garantir que não os perca e, em seguida, conclua o processo de redefinição. O acesso à carteira atual não será possível após a redefinição.
+ Tokens em %1$s Esta placa ou anel não são suportados em nenhuma rede devido a limitações de firmware.
+ Você está com dificuldades para escanear seu cartão ou anel?
+ Este cartão não foi projetado para funcionar com este aplicativo.
+ Defina um código de acesso para ativar a biometria.
+ Usar %1$s Para desbloquear sua carteira e aprovar ações sensíveis, como assinar transações. No caso de carteiras de hardware, ainda é necessário um cartão ou anel para assinar.
+ Taxa padrão
+ Ative a opção Taxa Padrão para definir as taxas de transação automaticamente e ignorar a página de taxas ao enviar fundos. Você sempre poderá retornar a esta página, se necessário.
+ Acesse as configurações para ativar a autenticação biométrica no aplicativo Tangem.
+ Ativar autenticação biométrica
+ Desativar %1$s Você precisará inserir sua senha para desbloquear o aplicativo e interagir com sua carteira.
+ Seu código de acesso será solicitado posteriormente para armazenamento seguro.
+ Isso apagará todos os códigos de acesso salvos na carteira. Você precisará inserir o código de acesso novamente para usar a carteira.
+ Remover os dispositivos salvos exclui todas as carteiras salvas e seus códigos de acesso do aplicativo.
+ Isso apagará todos os códigos de acesso salvos na carteira. Você precisará inserir o código de acesso novamente para usar a carteira.
+ Exigir código de acesso
+ Esta opção desativa a biometria para ações sensíveis. Você precisará inserir seu código de acesso sempre que assinar uma transação.
+ Salvar código de acesso
+ A autenticação biométrica será solicitada em vez do código de acesso para interações com seu cartão ou anel.
+ Mantenha a carteira no aplicativo.
+ Habilite a vinculação de todas as carteiras ao aplicativo Tangem. A autenticação biométrica será necessária para desbloquear o aplicativo. A assinatura da transação requer o uso do seu cartão ou anel Tangem.
+ Escuro
+ Claro
+ Padrão do sistema
+ Tema
+ Configurações do aplicativo
+ Adicionar carteira
+ Selecione uma carteira para fazer login.
+ Bem vindo de volta!
+
+ - UM
+ - OUTRO
+
+ Carteira móvel
+ Você fez o backup da sua carteira com sucesso.
+ Essas palavras não poderão ser recuperadas caso sejam perdidas. Guarde-as em local seguro.
+ Backup concluído
+ Sua frase secreta de recuperação é um conjunto de %s Palavras aleatórias para acessar e recuperar sua carteira.
+ Se você perder esta frase, ela não poderá ser recuperada. Guarde-a em local absolutamente seguro.
+ Mantenha-o em segurança.
+ Salve essas %s palavras em um local seguro que somente você possa acessar e nunca as compartilhe com ninguém.
+ Não há possibilidade de recuperação
+ Frase de recuperação
+ O %s As palavras abaixo são a frase de recuperação da sua carteira. Nunca compartilhe essas palavras com ninguém. A Tangem nunca as solicitará. Use-as para restaurar sua carteira caso perca seu dispositivo.
+ Anote isso %s Organize as palavras em ordem numérica e mantenha-as em segurança e privacidade.
+ Você é totalmente responsável por proteger sua carteira e fazer backup da sua frase de recuperação.
+ Frase de recuperação
+ Para ocultar ou exibir seus saldos, basta virar a tela do seu dispositivo para baixo ou desligá-la nas Configurações.
+ Não mostrar novamente
+ Entendi
+ Os saldos estão ocultos.
+ Segundo os desenvolvedores da blockchain, os tokens Kaspa estão atualmente em fase beta. Fique atento para mais novidades!
+ Modo Beta
+ A biometria está desativada no seu dispositivo, portanto, você não pode usá-la para desbloquear suas carteiras. Ative a biometria nas configurações do seu dispositivo para usar esse método novamente.
+ Autenticação biométrica desativada
+ Por favor, digitalize o cartão ou toque no sino.
+ Você atingiu o limite de tentativas biométricas. Por favor, desbloqueie sua carteira com um cartão/anel ou insira seu código de acesso.
+ Autenticação biométrica bloqueada
+ Por favor, tente novamente em 30 segundos, ou passe o cartão ou toque a campainha.
+ O login biométrico está temporariamente bloqueado. Tente novamente em 30 segundos ou desbloqueie sua carteira aproximando o dispositivo ou usando um código de acesso.
+ Muitas tentativas
+ Você desativou a autenticação biométrica no seu telefone e não poderá salvar carteiras no aplicativo. Para salvar carteiras, ative a função de autenticação biométrica nas configurações do seu telefone.
+ Os dados biométricos do seu dispositivo foram atualizados. Selecione sua carteira e insira o código de acesso para reativar o login biométrico.
+ Atenção necessária
+ Ocorreu um erro ao processar seu código promocional. Tente novamente mais tarde.
+ Erro de ativação
+ Seu código promocional foi ativado com sucesso. A recompensa será creditada em sua conta Bitcoin em até 14 dias.
+ Código promocional ativado
+ Este código promocional já foi utilizado e não pode ser ativado novamente.
+ Código indisponível
+ Este código promocional não é válido e não pode ser ativado.
+ Código inválido
+ É necessário um endereço Bitcoin para receber o bônus. Adicione um à sua carteira e tente ativar novamente.
+ Endereço de Bitcoin necessário
+ Iniciar processo de backup
+ Use um cartão bancário ou outros métodos de pagamento.
+
+ - dispositivo
+ - dispositivos
+
+
+ - token
+ - tokens
+
+ Reinicie o próximo dispositivo para continuar.
+ Reiniciar Carteira
+ Todos os dispositivos Tangem foram reinicializados. Agora você pode continuar atualizando sua carteira.
+ Atualize novamente
+ Reinicialização concluída
+ Recomendamos concluir o processo de reinicialização para todos os dispositivos Tangem nesta carteira.
+ Alguns dispositivos Tangem ainda precisam ser reinicializados.
+ Desative esta opção se não quiser que este cartão seja usado para redefinir códigos de acesso em outros cartões ou anéis nesta carteira. Observe que isso também impedirá que você redefina o código de acesso neste cartão.
+ Permite usar este cartão para redefinir o código de acesso de outros cartões nesta carteira.
+ Recuperação do código de acesso
+ Reiniciar
+ Tem certeza de que deseja fazer isso?
+ Alterar código de acesso
+ O código de acesso será alterado somente neste cartão ou anel.
+ Todos os dispositivos Tangem na carteira selecionada foram redefinidos para as configurações de fábrica. Agora você pode criar uma nova carteira.
+ Reinicialização concluída
+ Deseja redefinir o próximo dispositivo nesta carteira?
+ Redefinição da carteira
+ Recomendamos concluir o processo de reinicialização para todos os dispositivos Tangem nesta carteira.
+ Você não reiniciou todos os seus dispositivos Tangem.
+ Restaurar configurações de fábrica
+ Modo de segurança
+ Configurações do dispositivo
+ Além da taxa de rede, a rede Cardano cobra %1$s ADA ao realizar transações com o %2$s token
+ Requisitos de transação Cardano
+ Para fazer um %1$s Para realizar a transação, você precisa depositar alguns ADA para cobrir a taxa de rede e o valor mínimo de ADA (recomenda-se 5 ADA).
+ ADA insuficiente para transferência de tokens.
+ O valor enviado e o troco não podem ser inferiores a 1 ADA.
+ Você precisa manter alguns ADA porque possui tokens na blockchain Cardano.
+ Não há ADA suficiente
+ Aceitar
+ Acesso negado
+ Conta
+ Contas
+ Ativar
+ Adicionar
+ Adicionar ao portfólio
+ Adicionar token
+ Adicionar tokens
+ Adicionado
+ Endereço
+ Todos
+ Permitir
+ Quantia
+ Análises
+ e
+ Aplicar
+ Aprovação
+ Aprovar
+ Atenção
+ Redes disponíveis
+ Backup
+ Saldo%s
+ Saldo
+ autenticação biométrica
+ biometria
+ Falha ao criar a transação
+ Comprar
+ Vá para %1$s
+ Você não concedeu acesso à sua câmera. Ajuste suas configurações de privacidade.
+ Cancelar
+ Mudar
+ Selecione a conta
+ Escolha uma ação
+ Escolha a rede
+ Escolha o token
+ Escolha a carteira
+ Reclamar
+ Reivindique recompensas
+ Fechar
+ Em breve
+ Confirmar
+ Conectando
+ Contate o suporte
+ Entre em contato com o suporte da Tangem.
+ Entre em contato com o Suporte Visa
+ Continuar
+ Converter
+ Cópia
+ Copiar endereço
+ Criar
+ %1$s (%2$s)
+ Personalizado
+
+ - dia
+ - dias
+
+
+ - Dia
+ - Dias
+
+ Excluir
+ Desativar
+ Desabilitado
+ Desconectar
+ Feito
+ Editar
+ Ativar
+ Habilitado
+ Erro
+ taxa de recarga de rede
+ Trocar
+ Explorar
+ Explore o histórico de transações
+ Explorador
+ Falha ao receber a taxa
+ As taxas de rede são encargos que os usuários pagam para processar e confirmar transações. O valor da taxa pode ser afetado pela congestão da rede, pelo tamanho da transação e pela prioridade de execução. %s
+ Rápido
+ Mercado
+ Lento
+ Velocidade e taxa
+ Terminar
+ Esquecer
+ Grátis
+ De
+ De %s
+ Sincronizar endereços
+ Comece agora
+ Obter token
+ Ir para o provedor
+ Ir para o token
+ Entendi
+ Esconder
+ Mantenha-se em %s
+ hora
+ Importar
+ Em andamento
+ Mais tarde
+ Saber mais
+ %1$s falta
+ Endereço Bitcoin
+ Bloqueado
+ Carteiras bloqueadas
+ Rede principal
+ mês
+ Taxa de rede
+ O valor enviado será reduzido em %1$s (%2$s) para cobrir o nível de taxa selecionado
+
+ - UM
+ - OUTRO
+
+ Novo endereço
+ Notícias
+ Próximo
+ NFT
+ Não
+ Sem endereço
+ Nenhum resultado
+ Não adicionado
+ Não disponível
+ Agora não
+ Agora
+ OK
+ Abrir no navegador
+ ou
+ Cartão principal
+ Anel primário
+ Senha
+ Colar
+ política de Privacidade
+ %1$s-%2$s
+ %1$s — %2$s
+ Leia mais
+ Receber
+ Recomendado
+ Rejeitar
+ Recarregar
+ Renomear
+ Obrigatório
+ Reiniciar
+ Salvar
+ Salvar alterações
+ Procurar
+ Tokens de pesquisa
+ segundos
+ Veja tudo
+ Frase-semente
+ Selecione a ação
+ Vender
+ Enviar
+ Falha ao enviar a transação
+ O servidor não está disponível. Tente novamente mais tarde.
+ Compartilhar
+ Compartilhar link
+ Mostrar menos
+ Mostrar mais
+ Assine
+ Assine e envie
+ Pular
+ Algo deu errado
+ Stake
+ Apostas
+ Começar
+ Enviar
+ Sucesso
+ Suporte
+ Redes suportadas
+ Trocar
+ Tangem
+ Carteira Tangem
+ Toque e segure
+ termos e Condições
+ Termos de Uso
+ Para
+ Para %s
+ Hoje
+ Token a ser enviado
+
+ - token
+ - tokens
+
+ Transação falhou
+ Status da transação
+ Transações
+ Transferir
+ Não foi possível carregar os dados…
+ Eu entendo
+ Entendi, continue.
+ Ocorreu um erro. Por favor, tente novamente.
+ Desbloquear
+ Inalcançável
+ Unstake
+ Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade.
+ Valor copiado
+ Carteiras
+ semana
+ com
+ Sim
+ Modo de rendimento
+ Endereço do contrato copiado!
+ Redes disponíveis
+ A derivação do seu token corresponde à derivação de %1$sSeu token será adicionado a esta conta.
+ A derivação pertence a outra conta.
+ Token adicionado a %1$s conta
+ Endereço do contrato
+ O endereço do contrato é inválido.
+ Por favor, selecione a rede.
+ Este token já foi adicionado à lista.
+ O token já existe.
+ O valor decimal deve ser um número inteiro válido, até um limite de %d
+ Derivação personalizada
+ Exemplo: m/00\'/0000\'/0\'/0/0
+ Insira uma derivação personalizada
+ Decimais
+ Caminho de Derivação
+ Padrão
+ Tipo de moeda BIP44
+ O caminho de derivação que você inseriu não é válido.
+ Exemplo: Moeda USD
+ Nome
+ Não selecionado
+ Rede
+ Rede de tokens
+ Você pode adicionar manualmente um token que não seja suportado nativamente pelo Tangem.
+ Exemplo: USDC
+ Símbolo
+ Símbolo do token
+ Este token/rede já foi adicionado à sua lista.
+ Observe que qualquer pessoa pode criar tokens. Tenha cuidado ao adicionar tokens fraudulentos, pois eles podem não custar nada.
+ Tenha cuidado ao adicionar tokens fraudulentos, eles podem não custar nada.
+ Note que os tokens podem ser criados por qualquer pessoa.
+ Compre a carteira Tangem
+ Bate-papo
+ Código de acesso
+ Você precisará inserir o código de acesso correto antes de escanear o cartão.
+ Toque longo
+ Esse mecanismo protege contra ataques de proximidade em um cartão ou anel. Ele impõe um atraso entre o recebimento e a execução de um comando.
+ Código de acesso
+ Antes de executar qualquer comando que implique uma alteração no estado do cartão, você precisará inserir a senha.
+ Faça upgrade para uma carteira de hardware.
+ NFT
+ Programa de indicação
+ Vire a tela do seu dispositivo para baixo para ocultar e exibir rapidamente os saldos.
+ %s hashes
+ ID do dispositivo
+ Abra o chat de suporte
+ Link para mais cartões
+ Moeda do aplicativo
+ Virar para ocultar Saldos
+ Emissor
+ Assinado
+ Enviar feedback
+ Detalhes
+ Você só pode ter uma carteira móvel. Atualize para a carteira de hardware Tangem ou adicione uma nova carteira de hardware.
+ Verifique sua conexão com a internet ou mude para uma rede diferente.
+ Termos de serviço
+ Endereço padrão
+ Legado %s Endereço
+ Receber ativos
+ %s endereço
+ O envio de ativos por outras redes resultará em perda permanente.
+ %s rede
+ Envie fundos usando apenas
+ Melhores oportunidades
+ Limpar filtro
+ A lista está temporariamente vazia, pois está sendo atualizada. Volte daqui a pouco.
+ Todas as redes
+ Todos os tipos
+ Filtrar por
+ Minhas redes
+ Redes
+ Utilizado principalmente
+ Nenhum resultado
+ Ganhar
+ Olá equipe de suporte, encontrei um erro com o seguinte código: %s
+ Erro no WalletConnect
+ Você usou um cartão ou anel de outra carteira. Toque no cartão ou anel associado a esta carteira.
+ Não há fundos suficientes para esta transação. Por favor, adicione mais fundos à sua conta.
+ Troque qualquer ativo da sua carteira por este token.
+ Meus tokens
+ Você ainda não adicionou nenhum token. Adicione tokens pelo Mercado para trocar.
+ Não pode ser trocado por %s
+ Fornecido por
+ Status
+ Escolha o fornecedor
+ Os provedores facilitam as transações.
+ Fornecedor
+ Ocorreu um erro. Código: %s
+ Erro %1$sO provedor selecionado não pode processar a transação especificada. Arredonde o valor para cima. %2$s ou alterá-lo
+ O provedor selecionado está indisponível no momento. Tente novamente mais tarde. (Código: %s)
+ Trocas não estão disponíveis no momento. Tente novamente mais tarde. (Código: %s)
+ Valor estimado
+ Troca por %s
+ Acesse o site do fornecedor para solicitar o reembolso.
+ Operação falhou devido ao provedor
+ Sua conversão está demorando mais do que o normal, mas seus fundos estão completamente seguros e serão entregues. Em caso de dúvidas, entre em contato com a equipe de suporte do provedor.
+ Tempo de transação longo
+ O valor da transação foi reembolsado em %1$s para sua carteira devido às regras do OKX ou da ponte. %2$s
+ O valor foi reembolsado em %1$s (%2$s rede)
+ Visite o site do fornecedor para verificação.
+ Verificação KYC exigida pelo fornecedor
+ Compra concluída
+ Aguardando compra
+ Aguardando compra...
+ Transação cancelada
+ Depósito confirmado
+ Aguardando confirmação
+ Aguardando confirmação...
+ Troca concluída
+ Aguardando troca
+ Aguardando troca...
+ A troca falhou
+ Troca pausada
+ Depósito recebido
+ Aguardando depósito
+ Aguardando depósito...
+ Reembolso concluído
+ Aguardando reembolso
+ Enviando para você
+ Enviando de fundos...
+ Fundos enviados
+ Dados fornecidos pelo fornecedor. O valor estimado está sujeito a alterações devido às condições de mercado.
+ Status da troca
+ Verificação necessária
+ Aguardando hash da transação
+ Lista de todos os tokens adicionados à sua carteira.
+ Obtendo as taxas atuais...
+ taxa flutuante
+ Ao utilizar a funcionalidade de troca, você concorda com os termos do provedor. %s
+ Ao utilizar a funcionalidade de troca, você concorda com os termos do provedor. %1$s e %2$s.
+ Mais fornecedores estarão disponíveis em breve.
+ Fornecedor
+ Melhor tarifa
+ Lista de advertências da FCA
+ Tarifa competitiva
+ Fornecedor na lista de advertências da FCA
+ Disponível até %s
+ Disponível em %s
+ Indisponível para este par
+ Permissão necessária
+ Recomendado
+ Comprado %s
+ Comprando %s
+ Comprando %s...
+ Ocultar esta transação
+ Se você ocultar esta transação, ela não aparecerá mais na tela de status. Se você quiser apenas fechar a tela de status e retornar mais tarde, basta deslizar para o lado para descartá-la.
+ Ocultar o status da transação?
+ Este token não é compatível. Por favor, escolha um token diferente para usar em substituição.
+ %s não é suportado
+ Trocar com
+ Nenhum token encontrado. Tente novamente.
+ ID: %s
+ ID da transação copiado
+ Troque qualquer ativo da sua carteira por este token.
+ Maior velocidade significa confirmação mais rápida e uma taxa de rede mais alta. %s
+ Escolha a velocidade
+ Escolha qual token usar para pagar a taxa de rede. %s
+ Escolha o token
+ Mercado e Notícias
+ Em alta agora
+ As informações a seguir são opcionais. Você pode apagá-las se não quiser compartilhá-las.
+ Diga-nos quais funções estão faltando e tentaremos ajudá-lo.
+ Por favor, diga-nos qual cartão ou anel você possui.
+ Olá equipe de suporte,
+ Por favor, conte-nos mais sobre o seu problema. Cada pequeno detalhe pode ajudar.
+ Problema de backup
+ Carteira previamente ativada
+ Minhas sugestões
+ Não é possível escanear um Cartão/Anel
+ Opinião
+ Feedback Tangem
+ Não foi possível enviar uma transação.
+ Erro na descrição da moeda
+ Fundos insuficientes
+ Taxa de transação
+ Ocorreu um erro.
+ Ocorreu um erro. Código: %s.
+ Requer memorando
+ Transação
+ Ao aprovar, você permite que o contrato inteligente utilize seus tokens em transações futuras.
+ Quantia %s
+ A função Aprovar é necessária para conceder permissão a outro endereço para usar uma quantidade específica de seus tokens. Por definição, os contratos inteligentes não podem acessar seus tokens a menos que você aprove. Ao \"desbloquear\" seus tokens, você autoriza o contrato inteligente StakeKit a usá-los. Os mineradores da rede recebem uma taxa de gás (paga por você) para registrar essa ação no blockchain. Você pode fazer staking de seus tokens após conceder a aprovação.
+ Para continuar, você precisa permitir que o contrato inteligente da Polygon use seus dados. %s
+ Para continuar, conceda %1s permissão de contratos inteligentes para usar seu %2s
+ Conceder permissão
+ Ilimitado
+ Os endereços são gerados diretamente na sua carteira de hardware Tangem — prontos para usar e totalmente protegidos.
+ Novos endereços
+ Adicione a carteira Tangem
+ Sua chave privada será gerada diretamente dentro do cartão Tangem e nunca sairá dele.
+ Geração de chaves
+ Todas as operações criptográficas ocorrem dentro do chip de segurança, certificado contra clonagem e adulteração física.
+ Segurança em nível de hardware
+ Adicionar carteira existente
+ Criar nova carteira
+ Encomendar Tangem
+ Escanear Tangem
+ Deseja permitir que a \"Tangem\" utilize autenticação biométrica para confirmar sua identidade e abrir o aplicativo?
+ para %s
+ em %s rede
+ Tem certeza de que deseja cancelar a configuração do código de acesso?
+ Faça backup agora
+ Para concluir a configuração, faça backup da sua carteira e proteja o aplicativo com um código de acesso.
+ Finalize agora
+ Finalize a configuração da carteira.
+ Conclua a configuração protegendo o aplicativo com um código de acesso.
+ Se você sair, terá que começar tudo de novo.
+ Tem certeza de que deseja cancelar a ativação?
+ Mantém suas criptomoedas seguras e offline. Fino como um cartão de crédito, mais seguro que um cofre de banco.
+ Se fizer isso, terá que começar tudo de novo.
+ Recupere sua carteira existente por meio do backup do Google Drive.
+ Backup do Google Drive
+ Crie uma carteira segura e transfira seus fundos para obter proteção adicional.
+ Criar nova carteira
+ Aumente a sua segurança com a carteira de hardware superior da Tangem.
+ Carteira de hardware
+ Converta sua carteira móvel atual em uma carteira fria Tangem.
+ Atualize sua carteira atual
+ Ir para o backup
+ Faça um backup da sua carteira antes de criar um código de acesso.
+ Conclua o backup primeiro.
+ Finalize o backup primeiro.
+ Incompleto
+ Outros métodos
+ Guarde sua frase de recuperação em um local seguro e mantenha-a em sigilo para proteger seus fundos, e configure um código de acesso para segurança adicional.
+ Salve sua frase de recuperação em um lugar seguro e mantenha-a em sigilo para proteger seus fundos.
+ Frase de recuperação
+ Para proteger sua carteira com um código de acesso, conclua o processo de backup.
+ Para atualizar para uma carteira de hardware, conclua o processo de backup.
+ Atualize sua carteira móvel para uma carteira de hardware Tangem para obter a mais alta segurança. Importe sua carteira ou transfira fundos para uma nova.
+ Atualize para carteira fria
+ Suas chaves privadas são criptografadas com segurança e armazenadas em seu telefone.
+ As chaves privadas permanecem no seu dispositivo.
+ Crie ou restaure sua carteira com uma frase de recuperação.
+ backup da frase-semente
+ Criar carteira móvel
+ Transfira sua carteira móvel para um cartão ou anel Tangem a qualquer momento, com segurança.
+ Atualização para carteira de hardware
+ Importar carteira existente
+ Esta frase de recuperação já foi importada.
+ Carteira móvel
+ Esqueça a carteira
+ Esta carteira será removida permanentemente do seu dispositivo.
+ Tem certeza de que deseja fazer isso?
+ Esqueça a carteira
+ Ir para o backup
+ Ver frase de recuperação
+ Esqueça a carteira
+ Esqueça de qualquer forma
+ Esta carteira possui um backup. Certifique-se de que você pode recuperá-la antes de esquecer a carteira.
+ Se você esquecer esta carteira sem um backup, perderá permanentemente o acesso aos seus fundos.
+ Tem certeza de que deseja esquecer esta carteira?
+ Entendo que, se eu não fizer um backup da minha carteira antes de removê-la, perderei o acesso a ela.
+ Entendo que remover minha carteira não a exclui, apenas a remove do meu dispositivo.
+ Atualizar
+ Ao atualizar sua carteira digital para um cartão ou anel Tangem, todos os seus endereços e saldos permanecem totalmente acessíveis.
+ Mesmos endereços
+ Não é possível atualizar. Já existe uma carteira neste dispositivo.
+ Escolha outro dispositivo. Este não pode ser usado para a atualização.
+ Ocorreu um erro durante a operação.
+ Seus fundos permanecem seguros e totalmente acessíveis durante todo o processo.
+ Acesso a fundos
+ Após a atualização, sua carteira móvel é removida do aplicativo e armazenada em sua carteira de hardware Tangem. Sua frase de recuperação permanece com você.
+ Segurança geral
+ As chaves privadas serão transferidas do aplicativo para sua carteira de hardware Tangem.
+ Migração chave
+ Dispositivo de digitalização
+ Iniciar atualização
+ Você está prestes a atualizar para nossa carteira de hardware. Ela manterá seus ativos seguros em armazenamento offline.
+ Carteira Tangem
+ Faça upgrade para nossa carteira de hardware.
+ Proteja suas criptomoedas com a carteira de hardware de alta qualidade da Tangem.
+ Atualize a segurança da sua carteira para hardware.
+ Esta informação foi gerada com IA.\nToque aqui se encontrar algum erro.
+ Para alterar o código de acesso, aproxime o cartão ou toque-o conforme mostrado acima e não o remova até o final da operação.
+ Para alterar a senha, aproxime o cartão conforme mostrado acima e não o remova até o final da operação.
+ Para criar a carteira, aproxime o cartão conforme mostrado acima e não o remova até o final da operação.
+ Para criar a carteira, toque no anel conforme mostrado acima e não o remova até o final da operação.
+ Toque no cartão #%s da carteira
+ Toque para digitalizar
+ Toque para assinar
+ Toque no Cartão ou no Anel
+ Você atualizou seus dados biométricos. Escaneie seu cartão ou toque a campainha para entrar.
+ Seu saldo deve ser superior ao valor da taxa para efetuar uma transferência.
+ Saldo insuficiente
+ Você não possui Mana suficiente para esta transação. Aguarde até que sua Mana seja recarregada. Seu saldo de Mana é %1$s/%2$s
+ Mana insuficiente
+ Você só pode transferir %s devido ao limite de Mana imposto pela rede Koinos
+ Limite de mana
+ A rede Koinos exige Mana para o pagamento das taxas de rede. Você tem %1$s/%2$s Mana
+ Nível de mana
+ Para começar a rastrear seus criptoativos e transações, adicione tokens.
+ Gerenciar tokens
+ Leia o código QR para enviar fundos ou conectar-se a um aplicativo
+ Para acessar todas as redes, você precisa escanear o cartão.
+ Escaneie seu cartão ou toque
+ Aproveitar %1$s Taxas de serviço em swaps via Changelly a partir de fevereiro %2$s-%3$s
+ Trocar com Changelly, %s tarifas
+ Tokens
+ Adicionar
+ Editar
+ capitalização de mercado da moeda
+ Blockchain, a criptomoeda, foi inicialmente criada.
+ Rede nativa
+ A utilização de redes não nativas para tokens permite a interoperabilidade entre blockchains, possibilitando que ativos sejam utilizados em diversas aplicações descentralizadas e contratos inteligentes em diferentes plataformas. No entanto, isso geralmente envolve um custodiante ou contrato inteligente para manter o ativo original em segurança, introduzindo centralização e risco de contraparte.
+ Não é a blockchain original ou primária onde o token está hospedado.
+ Redes não nativas
+ Escolha redes
+ Carteira
+ Não foi possível encontrar este token. Você pode adicioná-lo manualmente.
+
+ - de carteira
+ - de carteiras
+
+ Remover
+ Por exemplo, Bitcoin
+ Seu portfólio foi atualizado.
+ O token selecionado está atualmente indisponível para ações na carteira de criptomoedas. Mas não se preocupe, você pode demonstrar seu interesse votando a favor dele.
+ Voto positivo
+ A carteira não suporta mais de uma rede.
+ Sobre a moeda
+ Para comprar, trocar ou receber este ativo, adicione-o à sua carteira.
+ Este ativo não é atualmente suportado na carteira.
+ Este recurso não está disponível para esta carteira.
+ Adicionar
+ APY %s
+ Meu portfólio
+ Mercado
+ Ganhe com a Tangem
+ Para gerar endereços para redes selecionadas, você deve escanear seu cartão ou anel da Tangem Wallet.
+ Para adicionar tokens, abra esta janela ou toque na barra de pesquisa.
+ Deslize para cima para explorar o mercado.
+ Descubra novas joias escondidas
+ Os dados desta seção são provenientes das seguintes redes: %s
+ Não foi possível carregar os dados…
+ Sem dados
+ Pulso do mercado
+ Ações rápidas
+ Pesquisar tokens
+ Resultado
+ Veja tokens com capitalização de mercado inferior a 100 mil dólares.
+ Mostrar tokens
+ Nenhum resultado
+ Selecione a rede
+ Selecione a carteira
+ 1 mês
+ 1 ano
+ 24 horas
+ 3 meses
+ 6 meses
+ 7 dias
+ Todos
+ Compradores experientes
+ capitalização de mercado
+ Ordenar por
+ Ganhadores
+ Perdedores
+ Tendências
+ Modo de rendimento
+ Fazer staking é a maneira mais fácil de receber recompensas em suas criptomoedas. %s
+ Ganhe até %s APY
+ Token adicionado
+ Sobre %s
+
+ - exchange
+ - exchanges
+
+
+ - Com base em avaliação
+ - Com base em avaliações
+
+ Site
+ Pressão de compra
+ A diferença entre o volume de compradores e o volume de vendedores.
+ Pressão de compra
+ Suprimento circulante
+ O número total de moedas disponíveis para negociação e em circulação no mercado.
+ Suprimento circulante
+ Não foram encontradas Exchanges
+ Troca
+ Cuidado
+ Arriscado
+ Confiável
+ Trocas
+ Compradores experientes
+ Compradores líquidos com o requisito adicional de terem pelo menos 100 transações de saída.
+ Compradores ativos
+ Avaliação totalmente diluída
+ O valor teórico total de uma criptomoeda, considerando que todas as moedas possíveis estivessem em circulação, incluindo aquelas que não estão atualmente em circulação.
+ Avaliação totalmente diluída
+ Data de Gênesis
+ Alto
+ Portadores
+ A variação no número de detentores de tokens dentro de um período de tempo específico.
+ Portadores
+ D
+ M
+ W
+ Y
+ Percepções
+ Links
+ Liquidez
+ A variação na quantidade de liquidez disponível para o token durante o período especificado.
+ Liquidez
+ Índice de liquidez
+ Listado em
+ Baixo
+ valor de mercado
+ O valor total de mercado de uma criptomoeda é calculado multiplicando-se o preço atual da moeda pelo número total de moedas em circulação.
+ valor de mercado
+ Posição no mercado
+ Posição no ranking de criptomoedas entre todas as moedas com base na capitalização de mercado.
+ Posicionamento de mercado
+ Fornecimento máximo
+ O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda.
+ Fornecimento máximo
+ Métricas
+ Links oficiais
+ Desempenho de preço
+ Repositório
+ Pontuação de segurança
+ A pontuação de segurança de um token é uma métrica que avalia o nível de segurança de uma blockchain ou token com base em diversos fatores e é compilada a partir das fontes listadas abaixo.
+ Social
+ Fornecimento total
+ O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda.
+ Fornecimento total
+ 24h
+ Volume de negociação (24h)
+ O valor total negociado de uma criptomoeda nas últimas 24 horas indica o nível de atividade e liquidez do mercado.
+ Volume de negociação (24h)
+ %s no total
+ Volume
+ Abra esta opção ou toque na barra de pesquisa para adicionar tokens diretamente do mercado.
+ Adicionar tokens
+ Adicionar mais tokens
+ Potencialize seus ativos, fornecendo-lhes acesso instantâneo. %s
+ Ativar o modo de rendimento
+ Você precisa atualizar para %1$s antes de criar uma carteira móvel
+ A carteira digital requer %1$s ou mais tarde
+ Todas as notícias
+ Gosto
+
+ - UM
+ - OUTRO
+
+
+ - UM
+ - OUTRO
+
+ Resumo rápido
+ Notícias
+ Tokens relacionados
+ Notícias relacionadas
+ Fique por dentro de tudo
+ Pontuação em alta
+ A tecnologia NFC não está disponível no seu dispositivo.
+ Sobre NFTs
+ Ativo NFT
+
+ - Artigo
+ - Artigos
+
+ Os NFTs enviados para o endereço da sua carteira aparecerão aqui.
+ Ainda não há coleções.
+ Receba NFTs
+ coleções NFT
+ Alguns dados podem não carregar.
+ Problemas temporários de carregamento
+ Informações básicas
+ Cadeia
+ Endereço do contrato
+ Chain é a blockchain onde o NFT existe.
+ O endereço do contrato é um identificador único para o contrato inteligente que rege os tokens na blockchain.
+ Uma etiqueta que descreve a raridade do NFT. Quanto menor o valor, mais exclusivo é o NFT.
+ A posição de um NFT no ranking de raridade em relação a outros tokens. Quanto maior a posição, mais raro e valioso é o NFT.
+ O endereço do token é um identificador exclusivo do token no blockchain, permitindo o rastreamento das transações e da propriedade
+ O ID do token é um identificador único atribuído a cada token, distinguindo-o dos demais na coleção.
+ O padrão do token define o tipo de token e como ele funciona com diferentes carteiras e plataformas.
+ Último preço de venda
+ Rótulo de raridade
+ Classificação de raridade
+ Endereço do token
+ ID do token
+ Padrão de token
+ Características
+ Nenhum resultado encontrado. Por favor, tente novamente.
+ Nenhuma coleção
+ Disponível
+ Escolha a rede
+ Receber NFT
+ Você ainda não adicionou esta rede. Para receber NFTs, adicione-a ao seu portfólio.
+ Rede não adicionada
+ Tipos de NFT não suportados
+ cNFTs e pNFTs ainda não são suportados. Por favor, não os envie para sua carteira.
+ Enviar NFT
+ Características
+ Coleção sem título
+ %1$d NFTs em %2$d coleções
+
+ - NFT em
+ - NFTs em
+
+ Toque aqui para receber o primeiro NFT
+ coleções NFT
+ Não foi possível carregar os dados.
+ Para usar o %1$s rede, você deve pagar a reserva da conta (%2$s %3$s), que bloqueia e oculta esse valor indefinidamente
+ A conta de destino não está ativa. Enviar %s ou mais para ativar a conta.
+ Para criar uma conta, envie fundos para este endereço.
+ A conta de destino não possui uma linha de confiança para o ativo que está sendo enviado.
+ Ganhe $10 em BTC com cada carteira \n Aproveite!
+ Black Friday: até 30% DESLIGADO
+ Vamos lá
+ Tempo limitado!
+ 1+1: Compre uma carteira e ganhe 50% DESLIGADO
+ Cadastre-se agora
+ Compartilhe seu código e ganhe 5 USDT por venda. Seu amigo ganha 10.% DESLIGADO.
+ Ganhe RECOMPENSAS por cada amigo!
+ Compre criptomoedas
+ Aproveite **0% taxas** na compra de criptomoedas via transferências SEPA.
+ Compre criptomoedas com SEPA
+ Entre na lista de espera e receba um cartão de pagamento como nenhum outro.
+ Cartão Visa Tangem
+ Termos e Condições
+ Deposite US$ 100 ou mais, mantenha por 30 dias e receba US$ 10.
+ Participe da campanha Yield Mode!
+ Configure um código de acesso único para proteger todos os seus dispositivos.
+ Proteger
+ Defina um código de acesso individual para cada cartão ou toque mais tarde.
+ Personalizar
+ Recupere o código de acesso com um cartão ou anel vinculado. Não guarde todos os seus dispositivos no mesmo lugar.
+ Restaurar
+ Escolha qualquer palavra, frase ou número que desejar como seu código de acesso.
+ Criar código de acesso
+ Digite seu código de acesso mais uma vez para evitar erros.
+ Digite novamente seu código de acesso.
+ O código de acesso deve ter pelo menos 4 caracteres.
+ O código de acesso inserido não corresponde ao código de acesso inicial.
+ Por favor, repita a operação. O cartão será redefinido para as configurações de fábrica.
+ Erro de ativação
+ Adicionar tokens
+ Você adicionou um cartão ou anel de backup. Depois que o backup for finalizado, você não poderá adicionar mais dispositivos. Se você tiver mais um cartão ou anel, adicione-o agora. Deseja continuar?
+ O backup está parcialmente concluído e não pode ser encerrado agora.
+ Uma frase-senha é um recurso de segurança opcional que adiciona uma palavra ou frase à sua frase de recuperação, criando um novo conjunto de endereços de carteira para proteção extra.
+ Adicione um Cartão ou Anel
+ Digitalizar cartão
+ Escanear Cartão #%d
+ Faça backup agora
+ Escanear Cartão
+ Digitalizar Anel
+ Continuar para minha carteira
+ Finalizar backup
+ Receba criptomoedas
+ Escaneie o Cartão principal ou o Anel.
+ Pular para mais tarde
+ Como funciona?
+ Vamos gerar todas as chaves do seu cartão ou anel e criar uma carteira segura.
+ Criar carteira
+ Criar uma carteira
+ Outras opções
+ Suas chaves serão geradas com segurança dentro do chip. Não há frase mnemônica, o que significa que ninguém pode exportá-las ou roubá-las.
+ Gere chaves de forma privada
+ Seu cartão está ativado e pronto para uso.
+ Sucesso!
+ Sua carteira está configurada e pronta para uso!
+ Nesse caso, você precisará começar do início.
+ Deseja sair do processo de ativação?
+ Começando
+ Já existe outra carteira associada ao cartão que você está tentando adicionar. Se você tiver fundos nessa carteira, faça o saque e, em seguida, redefina este cartão e adicione-o como reserva.
+ Salve sua carteira
+ Criando um backup
+ Biometria
+ Leia mais sobre a frase-semente.
+
+
+ - Escreva estas %dpalavras na ordem indicada abaixo e guarde-as em um local seguro e secreto.
+
+ Sua frase-semente
+
+
+ - %dpalavras
+
+ Para importar sua carteira, insira sua frase mnemônica no campo abaixo.
+ Gerar frase-semente
+ Importar carteira
+ Uma frase mnemônica é uma sequência de palavras que permite recuperar sua carteira. Ao contrário das chaves geradas pelo cartão ou anel, as frases mnemônicas não são protegidas e podem ser copiadas e roubadas. Use esta opção por sua conta e risco.
+ Use a frase-semente
+ Frase inicial inválida. Verifique a ordem das palavras.
+ Frase de inicialização inválida. Por favor, verifique sua ortografia.
+ Legado
+ Para verificar se você anotou sua frase-semente corretamente, digite a 2ª, a 7ª e a 11ª palavras.
+ Então, vamos verificar.
+ Para iniciar o processo de backup, adicione até dois cartões ou anéis de backup.
+ Você pode adicionar mais um cartão ou anel ou finalizar o processo de backup.
+ Prepare o cartão de backup com o número %s
+ Escaneie o cartão principal ou o anel para iniciar o processo de backup.
+ Prepare o cartão principal com o número %s
+ Prepare o Anel e pressione o botão de digitalização abaixo.
+ Sua carteira está configurada e pronta para uso.
+ Número máximo de dispositivos adicionados. Finalizar o processo de backup.
+ Ativando a carteira
+ Cartão de backup
+ Cartão de backup #%d
+ Anel de backup
+ Sem dispositivos de backup
+ Notificações
+ Um dispositivo de backup adicionado
+ Prepare seu Cartão ou Anel.
+ Dois dispositivos de backup adicionados
+ Para começar, basta adicionar qualquer valor à sua carteira.
+ Para começar, basta adicionar mais de um valor à sua carteira. %1$s %2$s
+ Compre criptomoedas
+ Mostrar o endereço da carteira
+ Para começar, basta recarregar a carteira com mais de %1$s
+ Ative uma carteira
+ O processo de geminação está parcialmente concluído. Não é possível sair dele agora.
+ Se o processo de criação da carteira for interrompido de alguma forma, você terá que começar tudo de novo.
+ Você pode fazer backup das suas chaves em até dois outros cartões ou anéis em branco da Tangem Wallet.
+ O código de acesso pode ser restaurado com um dos cartões de backup.
+ Todos os cartões de backup podem ser usados com todas as suas funções, utilizando as mesmas chaves.
+ Você poderá definir um código de acesso para proteger suas carteiras.
+ Carteira de backup
+ Restaurar código de acesso
+ Cartões idênticos
+ Código de acesso
+ Você só pode ter uma carteira móvel. Atualize para a carteira de hardware Tangem ou adicione uma nova carteira de hardware.
+ Todas as ofertas
+ Disponível com %s
+ Os provedores facilitam as transações.
+ Pesquisar por país
+ Indisponível
+ Outras moedas
+ Fiats populares
+ Pesquisar por moeda
+ Esta transação já foi processada. Nenhuma ação adicional é necessária.
+ Obtendo as melhores taxas...
+ Instantâneo
+ Ao utilizar a funcionalidade de acesso prioritário, você concorda com os termos do provedor. %1$s e %2$s
+ O serviço é fornecido por um provedor externo. A Tangem não se responsabiliza por ele.
+ O valor da compra não deve ser superior a %s
+ O valor da compra deve ser de pelo menos %s
+ Não há fornecedores disponíveis para esta moeda.
+ Processamento mais rápido
+ Pagar com
+ Método de pagamento
+ Disponível até %s
+ Disponível em %s
+
+ - UM
+ - OUTRO
+
+ Fornecedores
+ Usado recentemente
+ Recomendado
+ Você poderá concluir sua transação por meio do provedor terceirizado. %s
+ Redirecionando para %s...
+ Nossos serviços não estão disponíveis neste país.
+ Sua residência foi identificada como
+ Residência
+ Selecione o país correto para garantir opções de pagamento e serviços precisos.
+ Configurações
+ Você pode fechar esta tela e verificar o status da transação na tela de detalhes do token.
+
+ - até
+ - até
+
+ %s min
+ Disponível em
+ Disponível até
+ Você recebe
+ O serviço é fornecido por um provedor externo. A Tangem não se responsabiliza por ele.
+ Você pode fechar esta tela e verificar o status da transação na tela de detalhes do token.
+ Até
+ Via
+ via %s
+ Você pagará
+ Grupo
+ Por equilíbrio
+ Organizar tokens
+ Desagrupar
+ %s suporte
+ Mais informações
+ Você pode ativar as notificações do Tangem nas Configurações.
+ Ativar mais tarde
+ Configurações
+ Ativar notificações
+ Receba alertas de transações recebidas em redes compatíveis.
+ Notificações de transação
+ Selecione na galeria
+ Configurações
+ Você não concedeu acesso à sua câmera.
+ Acesso à câmera negado
+ Não é necessário memorando
+ %1$s (%2$s) sobre %3$s rede
+ %1$s sobre %2$s rede
+ O envio de qualquer outra moeda resultará em sua perda irreversível.
+ Enviar somente %s Para este endereço. O envio de qualquer outra moeda resultará em sua perda irreversível.
+ Enviar somente %1$s no %2$s rede
+ Transfira fundos de qualquer carteira ou corretora.
+ Endereço para recebimento de recompensas
+ Participar
+ Não foi possível carregar as informações sobre o programa de indicações. Tente novamente mais tarde.
+ Falha ao carregar as informações sobre o programa de indicações. Código de erro: %sPor favor, tente novamente mais tarde.
+ Pagamentos futuros
+ Seus amigos compraram
+ Menos
+ Mais
+ Não há pagamentos futuros
+
+ - para carteira
+ - para carteiras
+
+ Vou receber ^^%1$s^^ para cada carteira comprada pelo seu amigo no seu %2$s endereço de rede %3$s ^^30 dias depois^^ disso
+ Você
+ Vai receber um
+ ao comprar uma carteira em tangem.com
+ %s desconto
+ Seu amigo
+ Código pessoal copiado!
+ Seu código pessoal
+ Compre a carteira Tangem com desconto!\n%s
+ Indique a Tangem aos seus amigos.
+ Você aceitou
+ Ao tocar neste botão, você aceita.
+ do programa de indicação
+
+ - carteira
+ - carteiras
+
+ Reinicie o cartão
+ Entendo que, após realizar esta ação, não terei mais acesso à carteira atual.
+ Percebo que não posso usar este cartão para recuperar meu código de acesso nos outros cartões da carteira atual.
+ Entendo que perderei completamente o acesso ao meu cartão Tangem Pay e a todos os fundos nele contidos, sem possibilidade de recuperação.
+ A restauração de fábrica apaga completamente a carteira do cartão ou anel selecionado. Você não poderá restaurar a carteira atual nem usar este cartão ou anel para recuperar o código de acesso.
+ A restauração de fábrica apaga completamente a carteira do cartão ou anel selecionado e a remove do aplicativo. Você não poderá restaurar a carteira atual.
+ Todos os dispositivos Tangem foram reinicializados.
+ Ocorreu um problema no processo de ativação. Por favor, reinicie os cartões um por um.
+ Falha na verificação do cartão
+ Reinicie o próximo dispositivo para continuar.
+ Proprietários de Anéis têm direito a 3 trocas sem comissão na Changelly até 15/11!
+ Trocar por 0% Taxas já disponíveis!
+ Dispositivos com acesso root são considerados menos seguros. Seus dados podem ficar expostos a riscos adicionais.
+ Acesso root detectado
+ Faça login no aplicativo e consulte seu saldo sem precisar escanear o cartão ou tocar o anel.
+ Acesse o aplicativo
+ Permitir o uso de biometria
+ Para interagir com sua carteira, serão solicitados dados biométricos em vez do código de acesso.
+ Código de acesso
+ Não permita
+ Parece que você desativou a autenticação biométrica. É necessário salvar as carteiras.
+ Ativar autorização biométrica
+ Você gostaria de usar biometria?
+ Observe que, para realizar uma transação com seus fundos, você ainda precisará do seu cartão ou anel.
+ Escanear Cartão ou Anel
+ Escaneie o cartão ou anel para alterar suas configurações. As alterações afetarão apenas o cartão ou anel que você escaneou e não afetarão outros dispositivos vinculados à sua carteira.
+ Prepare seu Tangem!
+ Alerta de segurança
+ Não, eu não fiz isso.
+ Sim, me guie
+ Você não possui saldo suficiente para vender criptomoedas. Por favor, deposite o ativo desejado para prosseguir.
+ Saldo insuficiente
+ A venda de criptomoedas não está disponível na sua região no momento. Estamos trabalhando ativamente para disponibilizar essa opção em breve — fique atento!
+ Restrição Regional
+ Já incluído no endereço inserido
+ O valor da sua comissão é %s vezes superior à quantidade recomendada. Por favor, revise e ajuste suas configurações personalizadas.
+ Você especificou uma comissão abaixo do valor recomendado, o que pode causar atraso na sua transação. Deseja continuar?
+ Razão: %1$sCódigo: %2$s
+ A transação não foi concluída.
+ Trocar para outro token ou rede
+ Quantia
+ Será enviado ao destinatário.
+ Você pode definir sua taxa de transação ajustando o valor no campo Satoshi por vByte.
+ A taxa que será cobrada pela sua transação. Você pode definir o valor.
+ Taxa máxima
+ Este é o preço que você está disposto a pagar por unidade de gás. Quanto maior o preço do gás, mais rápido sua transação será processada. (Taxa de prioridade incluída)
+ Taxa prioritária
+ A taxa que um usuário pode pagar aos mineradores ou validadores para agilizar a inclusão de sua transação em um bloco.
+ A taxa cobrada para usar cada saída de transação não gasta (UTXO) na rede Kaspa. Quanto mais UTXOs você usar em uma transação, maior será a taxa.
+ KAS por UTXO
+ %1$s, %2$s
+ Etiqueta de destino
+ Insira o endereço
+ Nome ou endereço ENS
+ O endereço é o mesmo que o endereço da carteira.
+ O valor mínimo é %s
+ A mudança mínima é %s
+ Taxa inválida
+ O saldo mínimo é %s
+ A conta de destino não foi criada. O valor a ser enviado deve ser %s + taxa ou mais
+ Erro desconhecido
+ Memorando inválido. Não será adicionado à transação.
+ Memorando
+ Verifique sua conexão de rede.
+ Informações sobre tarifas de rede indisponíveis
+ Você envia
+ De %s
+ Limite de gás
+ Este é o valor máximo de gás que será gasto para concluir uma transação ou contrato. Um limite de gás evita cobranças inesperadas ou ilimitadas ao executar uma transação.
+ Preço do gás
+ Este é o preço que você está disposto a pagar por cada unidade de gás. Quanto maior o preço do gás, mais rápido sua transação será processada.
+ Máximo
+ Valor máximo
+ Taxa até
+ Memorando: %s
+ Memorando inválido
+ Cobertura da taxa de rede
+
+ - %d O token não é compatível com este endereço.
+ - %d Os tokens não são compatíveis com este endereço.
+
+ Nonce
+ Número único para cada transação. Use-o para reenviar ou cancelar uma transação pendente.
+ Insira o nonce...
+ Fundos insuficientes para a transferência, pois o total da taxa e do valor da transferência excede o saldo disponível.
+ O total excede o saldo
+ Um equilíbrio de pelo menos %s É necessário manter sua conta na blockchain para evitar riscos de segurança. Esse valor permanecerá em seu saldo e não poderá ser sacado.
+ Depósito existencial
+ O valor da sua comissão é %s vezes maior que a quantidade recomendada. Por favor, revise e ajuste suas configurações personalizadas.
+ A taxa personalizada é alta
+ Devido às peculiaridades do %1$s Na rede, a taxa para transferir o saldo total é mais alta. Para reduzir a comissão, você pode sair %2$s.
+ A taxa é mais alta
+ A conta do destinatário não está ativada. O valor mínimo da transferência deve ser igual ou superior ao saldo isento de aluguel. %1$s.
+ O saldo da sua conta não pode ser inferior ao valor do aluguel. Por favor, mantenha um saldo mínimo de pelo menos [valor omitido]. %1$s em sua conta ou retire todos os fundos.
+ A comissão incluída excede o valor da transferência, resultando em um valor negativo.
+ Valor inválido
+ O valor mínimo para envio é %1$sCertifique-se de que o saldo restante após o envio não seja inferior a %2$s.
+ A conta de destino não foi criada. Por favor, altere o valor a ser enviado.
+ O valor a ser enviado deve ser de pelo menos %s
+ Deixar %s
+ Reduzir por %s
+ Reduzir para %s
+ Informamos que a sua transação poderá sofrer atrasos devido a determinadas taxas.
+ Atrasos na transação são possíveis.
+ Devido a %1$s limitações apenas %2$s Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade.
+ Limitação de transação
+ Opcional
+ Alinhe o seu código QR com o quadrado para escaneá-lo. Certifique-se de escanear corretamente. %s endereço de rede.
+ Recente
+ Destinatário
+ Endereço inválido
+ Certifique-se de que o endereço do destinatário esteja no **o %s rede** para evitar a perda de seus fundos
+ Enviar para
+ Uma etiqueta de memorando/destino é um ID exclusivo para diferenciar transações enviadas para o mesmo destinatário na mesma rede. **Atenção: a omissão de um memorando pode levar à extravio de fundos.**
+ Memo/Destination Tag é um código que separa as transações destinadas a um destinatário comum em uma rede criptográfica.
+ Atenção: O não cumprimento de um memorando pode levar à perda de fundos.
+ Minhas carteiras
+ Uma forma de medir as taxas de transação do Bitcoin. Indica a quantidade da menor unidade de Bitcoin (Satoshi) por byte virtual em uma transação. Quanto maior o número, mais rápido a transação será processada pelos mineradores.
+ Satoshi / vByte
+ Enviando...
+ Toque em qualquer campo para alterá-lo.
+ Enviar %s
+ Você está enviando **%1$s** incluindo uma taxa de rede de %2$s.
+ Você está enviando **%1$s** e %2$s
+ Você está enviando **%1$s**
+ A taxa de rede será coberta pelo uso. %1$s energia
+ A taxa de rede será reduzida ao gastar %1$s energia
+ incluindo uma taxa de rede de %1$s
+ Para endereço
+ A transação foi assinada e enviada com sucesso para o nó da blockchain. O saldo da carteira será atualizado em breve.
+ %1$s Tron é um ativo na rede Tron. Para calcular a taxa e realizar uma transação, você precisa depositar alguns Tron (TRX) em sua conta.
+ O valor excede o saldo.
+ É necessário um marcador de destino (memorando) para concluir esta transação para o endereço especificado.
+ Etiqueta de destino obrigatória
+ Valor inválido
+ A taxa excede o saldo
+ O valor total excede o saldo.
+ Tem certeza de que deseja alterar o token de recebimento? Isso redefinirá os dados inseridos anteriormente.
+ Alterar token
+ Trocar e enviar
+ Deseja prosseguir com a troca? Isso apagará seus dados anteriores.
+ Confirmar conversão
+ O envio de qualquer outra moeda resultará em sua perda irreversível.
+ Selecione a rede de destinatários correta.
+ Escolha qualquer token para receber. Seu destinatário receberá exatamente o que você selecionou — sem complicações.
+ O destinatário receberá
+ Para o destinatário
+ Valor a receber
+ O destinatário recebe %s
+ Tem certeza de que deseja cancelar a conversão? Seus dados anteriores serão apagados.
+ Remover conversão
+ Enviar com troca
+ Transação enviada
+ Prepare-se para escanear o Cartão ou Anel que deseja configurar.
+ Esqueça a carteira
+ Isso removerá a carteira do aplicativo. A carteira poderá ser adicionada novamente.
+ Fácil de usar
+ Mantém suas criptomoedas seguras e offline. Fino como um cartão de crédito, mais seguro que um cofre de banco.
+ Sem frase-semente
+ A melhor carteira de hardware da categoria
+ Carteira fria Tangem
+ Nome
+ Coloque seu token para trabalhar.
+ Uma taxa de rede é um pequeno pagamento necessário para processar e confirmar sua transação na blockchain.
+ Para começar a fazer staking, sua conta TON precisa ser ativada com uma transação de 1 TON. Os fundos permanecem em sua conta, pois esta etapa apenas a ativa para staking.
+ Ativação de conta
+ A taxa de rede foi alterada. Por favor, verifique o novo valor antes de prosseguir.
+ Taxa de rede atualizada
+ O valor do staking deve ser de pelo menos %s
+ O valor de staking será arredondado para %1$s TRX devido às regras da rede.
+ O valor do resgate será arredondado para %1$s TRX devido às regras da rede.
+ APR %1$s%%
+ Suas recompensas de staking começarão após 5 épocas (aproximadamente 25 dias), enquanto sua delegação estiver registrada e contabilizada pela rede.
+ Reivindicação staking
+ Taxa de conta de staking
+ Uma conta de staking é uma conta especial onde os tokens SOL em staking são armazenados. Ela é criada quando você delega seus tokens a um validador para participar da validação de transações e receber recompensas. Uma pequena taxa é cobrada pela criação da conta de staking, a qual é devolvida após a conclusão do staking.
+ Taxa percentual anual
+ APR: Mostra a taxa de juros que você poderia ganhar em um ano sem capitalização. Os juros ganhos não são adicionados ao seu saldo, portanto, seus rendimentos não aumentam por si só.
+ Rendimento percentual anual
+ APY: Mostra o total de juros que você poderia ganhar em um ano com capitalização. Capitalização significa que os juros ganhos são adicionados ao seu saldo, de forma que você também ganha juros sobre esses juros.
+ APR
+ APY
+ As recompensas acumulam-se automaticamente no seu saldo de staking diariamente.
+ As recompensas são acumuladas no seu saldo de staking. Fundos ganhos: %s
+ Disponível
+ Taxa média de recompensa
+ Como funciona o staking?
+ %s lucro estimado
+ Posição no mercado
+ Métricas
+ De acordo com %1$s regras de rede, reivindicações são possíveis de %2$sOs valores abaixo serão creditados em sua conta após o resgate dos ativos.
+ Requisito mínimo
+ Nenhuma recompensa disponível
+ Reivindicação de recompensa
+ Método de recebimento de recompensas de staking. Pode ser automático, em que a recompensa é creditada em seu endereço, ou manual, em que você precisa sacar a recompensa criando uma transação para recebê-la.
+ Cronograma de recompensas
+ Este é um cronograma que determina quando os participantes do staking recebem suas recompensas. O tempo de distribuição das recompensas pode variar ligeiramente dependendo do validador e da carga da rede.
+ Recompensas: %s
+ Staking %s
+ Período de Desvinculação
+ O período que você deve aguardar após solicitar o saque de fundos do staking para que os tokens fiquem disponíveis.
+ Período de aquecimento
+ O período alocado para ativar a participação no staking.
+ Não há validadores disponíveis no momento. Tente novamente mais tarde.
+ Staking indisponível
+ A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para staking.
+ Ao utilizar a funcionalidade de staking, você concorda com os termos do provedor. %1$s e %2$s
+ Trancado
+ Quantidade máxima: %s
+ Migrar
+ Staking nativo
+ Não há validadores ativos disponíveis para staking no momento. Tente novamente mais tarde.
+ Ao fazer staking na rede Cardano, todo o seu saldo é utilizado. 2 ADA adicionais serão reservados e devolvidos após o desbloqueio. Seus ADA permanecem desbloqueados durante o staking.
+ Detalhes de engaste da ADA
+ As recompensas ganhas serão enviadas para sua carteira e estarão disponíveis para uso imediatamente.
+ A Tangem permite que os usuários façam staking de suas criptomoedas.
+ A Tangem permite que os usuários façam staking de suas criptomoedas.
+ A Tangem permite que os usuários façam staking de suas criptomoedas.
+ A Tangem permite que os usuários façam staking de suas criptomoedas.
+ O staking permite que você receba %1$sSuas recompensas de staking chegam todos os dias.
+ O staking permite que você receba %1$sSuas recompensas de staking chegam a cada hora.
+ O staking permite que você receba %1$sSuas recompensas de staking chegam todo mês.
+ O staking permite que você receba %1$sSuas recompensas de staking chegam toda semana.
+ A Tangem permite que os usuários façam staking de suas criptomoedas.
+ Ganhe recompensas com staking
+ Seu saldo em staking restante será muito baixo para ser retirado. Você precisará fazer staking de mais fundos para atingir o valor mínimo necessário para a retirada.
+ Saldo baixo em staking
+ No mínimo, %1$s %2$s É necessário refazer o cadastro. Por favor, recarregue seu saldo.
+ Não é suficiente %s
+ Saldo insuficiente para staking
+ É necessário um mínimo de 3 ADA para refazer a reserva. Por favor, recarregue seu saldo.
+ Não há ADA suficiente
+ O valor mínimo para staking deve ser superior a 5 ADA. Por favor, adicione fundos ao seu saldo para começar a fazer staking.
+ O staking está indisponível no momento devido a condições da rede. Tente novamente mais tarde.
+ Staking no %1$s Uma rede com um novo validador transferirá automaticamente todos os fundos previamente alocados para esse validador.
+ Reinveste as recompensas ganhas no valor apostado, aumentando os ganhos potenciais.
+ O Restake permite que você transfira seus fundos de um validador para outro sem precisar remover seus fundos do staking.
+ Se você fizer staking de todo o seu saldo, precisará pagar uma taxa de rede ao retirar os fundos. Recomendamos deixar um pequeno valor em sua carteira para cobrir essas taxas.
+ Para começar a fazer staking, você precisa ativar sua conta TON primeiro.
+ Ativação de conta
+ Para começar a fazer staking de TON, primeiro envie uma pequena transação para o seu próprio endereço — isso ativará sua carteira.
+ Poderá ser necessário um valor adicional de até 0,2 TON, além da taxa de rede, para concluir a transação. Qualquer valor não utilizado será reembolsado.
+ Para prosseguir com esta operação, é necessário um saldo de 0,2 TON, além da taxa de rede. Por favor, recarregue seu saldo.
+ Reserva TON necessária
+ Essa ação encerrará outras posições ou as converterá para o status de saque, de acordo com as regras da rede.
+ Status das posições
+ Desbloqueie seu dinheiro para retirá-lo do processo de staking. O desbloqueio leva... %s.
+ Seus fundos estarão disponíveis para uso após o período de desbloqueio de 21 dias. A recompensa será retirada juntamente com os fundos que você retirou do staking.
+ Seus fundos estarão disponíveis para uso após o %s período de desvinculação.
+ Você já pode sacar seus fundos, eles estarão disponíveis para uso imediatamente.
+ Ao fazer staking na rede Tron com um novo validador, todos os fundos previamente em staking serão transferidos automaticamente para esse validador.
+ Preparando
+ Pronto para retirar
+ Rebond
+ Repetir
+ Recompensas de reinvestimento
+ Revogar
+ Revote
+ Auto
+ Manual
+ Por bloco
+ Diário
+ Cada %s
+ Todos os dias
+ Cada %s dias
+ Cada
+ Por época
+ Por Era
+ Por hora
+ Mensal
+ Semanal
+ Recompensas
+ As recompensas na Solana são adicionadas automaticamente ao seu saldo de staking e não podem ser exibidas separadamente.
+ Stake bloqueada
+ Aposte mais
+ Ao fazer apostas %1$s, todo o seu %2$s O saldo é colocado em staking. Qualquer valor adicional %2$s Os depósitos que você fizer em sua carteira Tangem também serão automaticamente contabilizados em staking.
+ valor de staking
+ Você aposta %1$s e receberá sua recompensa. %2$s
+ Toque para desbloquear
+ Toque para desbloquear ou votar
+ Toque para retirar
+ Stake %s
+ Desvincule %s
+ A transação está sendo processada! A validação está em andamento na blockchain. Isso pode levar alguns minutos.
+ Desvinculação
+ Desbloquear
+ Desbloqueando
+ O valor a ser retirado do staking deve ser de pelo menos %s
+ O valor excede o saldo de stake
+ Sem staking
+ Unstaking
+ Retirar ativos de staking %s
+ Validador
+ Validadores
+ Parceiro estratégico
+ Voto
+ Voto
+ Retirar
+ Suas apostasSeus stakes
+ Armazene seus criptoativos com segurança, mantendo as chaves privadas dentro do seu cartão ou anel.
+ Carteira de hardware revolucionária
+ Adicione até 3 cartões ou anéis a uma carteira.
+ Backup ultrasseguro
+ Uma carteira de hardware para seus Bitcoins, Ethereums e muitas outras criptomoedas simultaneamente — tudo em um único cartão ou anel.
+ Milhares de moedas
+ Use em qualquer lugar, a qualquer hora. Sem fios nem baterias. Basta encostar o cartão ou ligar o telefone quando precisar das suas criptomoedas.
+ A carteira para todos
+ Conheça Tangem
+ Mais de 100 integrações de serviços descentralizados estão disponíveis.
+ Compatível com Web 3.0
+ Uma transação de entrada de pelo menos %1$s é necessário prosseguir
+ Fundos insuficientes
+ Ao aprovar, você permite que o contrato inteligente utilize seus tokens em transações futuras.
+ Taxa fixa
+ A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para a troca.
+ Troque mais tokens com melhores taxas diretamente na sua carteira.
+ Novo provedor de swaps disponível!
+ Procurando algo diferente?\n Experimente pesquisar ou explore outra criptomoeda!
+ Pesquise qualquer token, mesmo que ele ainda não esteja na sua lista.
+ Use a busca para encontrar o que você precisa.
+ Sinta-se seguro com o suporte disponível 24 horas por dia, 7 dias por semana, para ajudá-lo com qualquer problema.
+ Sempre aqui
+ Diversos provedores confiáveis em um só lugar — troque qualquer ativo facilmente em sua carteira.
+ Troque conosco
+ Sem erros, sem perdas de posse, sem pontos cegos — sua transação está sempre protegida.
+ Defesa Impenetrável
+ Maximize o valor do seu investimento com tarifas provenientes de uma ampla rede de fornecedores confiáveis, escolhendo sempre a melhor opção.
+ Tarifas imbatíveis
+ Sem complicações e intuitivo, permitindo que você troque tokens com apenas alguns toques
+ Simplesmente conveniente
+ Trocar através do provedor
+ Seus ativos
+ O valor inclui:\n• taxa do provedor de serviços\n• taxa de rede para envio %s da central de distribuição de volta para o endereço do usuário.
+ O valor inclui:\n• taxa do provedor de serviços\n• taxa de rede para envio %1$s da central de câmbio de volta para o endereço do usuário. \n\nO deslizamento do provedor depende de %2$s
+ O valor inclui a taxa do prestador de serviços.
+ O valor inclui a taxa do prestador de serviços. A margem de atraso do prestador é de até %s
+ Informação
+ Todas as exchanges descentralizadas exigem aprovações para impedir que contratos inteligentes acessem sua carteira sem sua permissão. Por definição, os contratos inteligentes não podem acessar seus tokens a menos que você os aprove. Ao \"desbloquear\" seus tokens, você autoriza o contrato inteligente 1-inch a gastá-los. Os mineradores da rede recebem uma taxa de gás (paga por você) para registrar essa ação no blockchain. Você pode trocar seus tokens após conceder a aprovação.
+ Aprovar
+ Erro na estimativa de custos. Por favor, envie seu feedback para o suporte.
+ Você troca
+ Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado.
+ Alto impacto nos preços
+ Fundos insuficientes
+ Conceder permissão
+ Trocar
+ Trocar...
+ Você recebe
+ Escolha o token
+ não disponível
+ Teremos todo o prazer em receber seu feedback.
+ O Tangem Pay agora está em versão beta.
+ Cartão bloqueado
+ Pagamento com cartão
+ Depósito
+ Disputa
+ Explorar transação
+ Taxas de serviço
+ Taxa
+ Proteja seu dinheiro. Você pode desbloqueá-lo a qualquer momento.
+ Congelar o seu cartão?
+ Não foi possível bloquear o cartão. Tente novamente mais tarde.
+ Congelar
+ Seu cartão está bloqueado.
+ Obtenha ajuda
+ Razão: %s
+ %s · %s
+ MCC %s
+ Outro
+ Não é possível usar em dispositivos com root.
+ Concluído
+ Recusado
+ Pendente
+ Revertido
+ Termos, taxas e limites
+ Termos e Limites
+ O banco rejeitou esta solicitação de transação.
+ Essa taxa destina-se a cobrir os custos de processamento da sua transferência.
+ A transação foi parcial ou totalmente revertida pelo comerciante.
+ Continue usando seu dinheiro. Você pode congelar a qualquer momento.
+ Descongelar seu cartão?
+ Não foi possível desbloquear o cartão. Tente novamente mais tarde.
+ Seu cartão foi desbloqueado.
+ Retirada
+ Não é possível usar em dispositivos com root.
+ Ocultar KYC da tela principal
+ Adicionar fundos
+ Opções de recarga
+ Adicionar ao Google Wallet
+ Número do cartão
+ Alterar PIN
+ O cartão está totalmente pronto para pagamentos.
+ Código PIN criado
+ CVC
+ Não foi possível carregar os dados. Tente novamente mais tarde.
+ Termo
+ Congelar Cartão
+ Ocultar detalhes
+ Esconder
+ Abra a Carteira do Google
+ Configure o Tangem Pay em poucos toques e comece a pagar com o Google Pay.
+ Configure o Tangem Pay em poucos toques e comece a pagar com o Apple Pay.
+ Adicione seu cartão ao Google Pay
+ Adicione seu cartão ao Apple Pay
+ Abra a Carteira do Google
+ Toque no botão “+” no canto superior direito.
+ Abra a Carteira da Apple
+ Toque em “Adicionar um cartão”
+ Toque em “Cartão de Débito ou Crédito”
+ Insira os dados do cartão manualmente.
+ Verifique o cartão usando o código OTP enviado para o seu dispositivo.
+ Tudo pronto! Seu cartão está pronto para uso.
+ Adicionar cartão ao Google Pay
+ Adicionar cartão ao Apple Pay
+ Código PIN
+ Compartilhe seu endereço ou mostre o código QR.
+ Problemas técnicos detectados. Tente novamente mais tarde ou entre em contato com o suporte.
+ Receber indisponível agora
+ Revelar
+ Mostrar detalhes
+ Troque qualquer ativo da sua carteira por um cartão.
+ Detalhes do cartão
+ Descongelar cartão
+ Volte ao aplicativo se você se esquecer.
+ Seu código PIN
+ Retirar
+ Retirada indisponível agora
+ Você não pode iniciar uma troca ou um novo saque até que o atual seja concluído.
+ Retirada em andamento
+ Alterar código PIN
+ Volte ao aplicativo se você se esquecer.
+ Entendo que perderei completamente o acesso ao meu cartão Tangem Pay e a todos os fundos nele contidos, sem possibilidade de recuperação.
+ Falha na emissão do cartão
+ Ocorreu um erro técnico. Tente novamente clicando no botão abaixo.
+ Ocorreu um erro técnico. Por favor, entre em contato com o suporte.
+ Obtenha seu cartão virtual Visa Tangem grátis.
+ Obtenha o Tangem Pay
+ Acesse o Suporte
+ Geralmente leva até 15 minutos.
+ Configurando seu cartão Tangem
+ Emissão do seu cartão
+ O cartão geralmente é emitido automaticamente em até 5 minutos. Em casos raros, se for necessária uma análise manual, o processo pode levar até 48 horas.
+ Tangem Pay
+ Confirmar cancelamento
+ Tem certeza de que deseja interromper o processo KYC? Você pode retornar a ele a qualquer momento.
+ Não foi possível verificar seu perfil. Caso tenha alguma dúvida, entre em contato com o suporte.
+ Infelizmente, não foi possível verificar sua identidade.
+ KYC rejeitado
+ KYC em andamento
+ Ver status
+ O processo KYC para Tangem Pay está em andamento.
+ Os documentos geralmente são verificados automaticamente em até 5 minutos. Em casos raros, se for necessária uma revisão manual, esse processo pode levar até 48 horas.
+ Rejeitado
+ Ocultar bloco KYC
+ Desculpe, não foi possível verificar.
+ Seu perfil.
+ Obtenha seu cartão virtual Visa Tangem grátis.
+ Use USDC para pagamentos do dia a dia.
+ Obter cartão
+ Com cartão digital compatível com Apple Pay e Google Pay.
+ Gaste seus ativos em qualquer lugar.
+ Não há taxas adicionais para compras.
+ Pague exatamente o que você vê.
+ Uma conta de pagamento separada será criada sem divulgar seus endereços e bens.
+ Privacidade incomparável
+ Obtenha seu cartão Tangem Pay gratuito em minutos.
+ Conta de pagamento
+ A conta de pagamento não está sincronizada.
+ PIN inválido: evite sequências ou repetições.
+ Estamos resolvendo um problema técnico. Por favor, tente novamente mais tarde.
+ Serviço temporariamente indisponível
+ Não foi possível exibir os detalhes. No entanto, os pagamentos com cartão ainda estão funcionando.
+ Defina o código PIN.
+ Não sincronizado
+ Restaurar acesso
+ Use USDC para pagamentos do dia a dia.
+ O serviço Tangem Pay está temporariamente inacessível.
+ Tangem Pay
+ Clique no botão abaixo para restaurar o acesso.
+ O saldo do seu Polygon em USDC na blockchain é diferente do saldo do seu cartão e é atualizado em até 2 dias úteis após uma compra. Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras.
+ Observe
+ Seu código PIN
+ Esta é a minha carteira.
+ Saldos ocultos
+ Saldos apresentados
+ Desfazer
+ Esta operação está indisponível no momento. Tente novamente mais tarde.
+ Comprando %s Não é suportado pelos fornecedores atuais, mas estamos trabalhando para adicionar mais opções.
+ A ação não está disponível para tokens personalizados.
+ Você não tem fundos para vender. Adicione fundos à sua conta para poder vender seus ativos.
+ Você não tem fundos para enviar. Recarregue sua conta para poder enviar fundos.
+ Os dados estão sendo carregados. Isso pode levar alguns segundos. Tente novamente mais tarde.
+ Troca %s Não é suportado pelos fornecedores atuais, mas estamos trabalhando para adicionar mais opções.
+ O saldo exibido pode estar desatualizado devido ao armazenamento em cache.
+ Os fundos para venda estarão disponíveis assim que a(s) transação(ões) pendente(s) na plataforma forem concluídas. %s A rede está completa.
+ O envio de fundos estará disponível assim que a(s) transação(ões) pendente(s) estiver(em) na rede. %s está completo
+ Vendendo %s Não é suportado pelos fornecedores atuais, mas estamos trabalhando para adicionar mais opções.
+ O Staking %s não é suportado pelos provedores atuais, mas estamos trabalhando para adicionar mais opções.
+ A aprovação foi revogada. Seus fundos permanecem no modo Yield. Para realizar ações, acesse o modo Yield e conceda a permissão novamente.
+ Gerar XPUB
+ Ocultar
+ Você está prestes a ocultar este token da tela principal. Você pode adicioná-lo novamente a qualquer momento através da página de gerenciamento de tokens.
+ Ocultar %s
+ Ocultar token
+ O staking permite que você receba %1$s e receba recompensas todos os dias %2$s dias
+ Serviço de staking
+ %1$s token em %%imagem%% %2$s rede
+ Token em %%imagem%% %1$s rede
+ O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista
+ Não foi possível ocultar %s
+ Mostrar código QR
+ Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s.
+ Trocar com Changelly, %s tarifas
+ Troque agora
+ Tendência de mercado 🔥
+ Não disponível para compra.
+ Não disponível para venda
+ Não disponível para troca a partir de %s
+ Indisponível para troca
+ contrato: %s
+ Você ainda não possui nenhuma transação.
+ Falha ao carregar o histórico de transações. Clique no botão \n para atualizar as informações.
+ Vários endereços
+ O histórico de transações ainda não é suportado nesta blockchain. Mas não se preocupe, estamos trabalhando nisso! Enquanto isso, você pode conferir no explorador.
+ Operação
+ para: %s
+ de: %s
+ para: %s
+ validador: %s
+ As notificações estão ativadas, mas não funcionarão até que você as permita nas configurações do seu dispositivo.
+ Notificações de transação
+ Mínimo %s
+ O valor mínimo da transação é %1$s.
+ As taxas da rede Tron para tokens populares podem ser mais altas. Fazer staking de TRX pode ajudar a reduzir os custos de transação.
+ Economize nas taxas da rede Tron
+ Tente novamente
+ Você escaneou o mesmo cartão. Para criar uma carteira gêmea, você precisa escanear o cartão com o número %d
+ Você escaneou o cartão errado. Por favor, tente outro.
+ Esta que você está segurando em suas mãos e a outra com o número %scartões podem ser usados para retirar fundos desta carteira.
+ Uma carteira. Dois cartões.
+ Escanear Cartão #%s
+ Criando carteira
+ Escaneie o #%s carta gêmea
+ Preparando o cartão
+ Gêmeo Tangem
+ Essa ação é irreversível. Você não terá mais acesso à sua carteira antiga.
+ Toque no cartão gêmeo com o número %s e não remova até o final da operação.
+ Por favor, tente novamente mais tarde. Se o problema persistir, entre em contato com o suporte.
+ Algo deu errado!
+ Ocorreu um erro. Código do erro: %sPor favor, entre em contato com nosso suporte.
+ Usar %s ou escaneie um cartão/anel para acessar sua carteira.
+ Falha na conexão: Este aplicativo descentralizado (dApp) usa a versão 1.0 do Wallet Connect, que não é compatível. Certifique-se de que o dApp seja compatível com a versão 2.0 do Wallet Connect para que a conexão seja estabelecida com sucesso.
+ Faça upgrade para uma carteira de hardware.
+ Fique por dentro das últimas novidades e recursos.
+ Alertas em tempo real para transações, câmbio e atualizações críticas.
+ Alertas de transação
+ Receba notificações de transações recebidas.
+ Seja o primeiro a saber sobre novas promoções.
+ Acesso antecipado a novas funcionalidades e ofertas exclusivas.
+ Atualizações de notícias e recursos
+ Você gostaria de usar notificações push?
+ Ative as notificações push para receber alertas quando os fundos chegarem à sua carteira.
+ Não perca nenhuma transação.
+ Adicionar carteira
+ Se você excluir esta carteira sem fazer um backup, perderá permanentemente o acesso aos seus fundos.
+ Tem certeza de que deseja esquecer esta carteira?
+ Ocorreu um erro. Por favor, digitalize seu cartão ou toque para fazer login.
+ Esta carteira já foi salva, você pode adicionar outra.
+ A carteira com nome %s já existe
+ Nome da carteira
+ Renomear carteira
+ Desbloquear tudo
+ Desbloqueie tudo com %s
+ Verificado AML
+ Disponível
+ Bloqueado
+ Dívida
+ Limites
+ Outros (no-otp)
+ Transação única
+ Total
+
+ - disponiver para
+ - disponivel para
+
+ O saldo disponível representa os fundos efetivamente disponíveis, considerando as transações pendentes, os valores bloqueados e o saldo devedor para evitar descobertos.
+ Disponível até %1$s
+ Saldos e limites
+ Informações da conta de pagamento ausentes. Entre em contato com o suporte.
+ Não foi possível carregar as informações sobre o token.
+ As informações ainda estão sendo carregadas, aguarde.
+ É necessário estabelecer limites para controlar custos, melhorar a segurança e gerenciar riscos. Você pode gastar %1$s durante o período para pagamentos com cartão em lojas e %2$s para outras transações, como assinaturas ou dívidas.
+ O código de acesso será usado para gerenciar sua conta de pagamento e protegê-la contra acessos não autorizados.
+ Código de acesso
+ Ativação de conta
+ Por favor, selecione a carteira que você utilizou para iniciar o processo de registro para assinar a transação e criar sua conta na Blockchain.
+ Cancelar ativação
+ Tem certeza de que deseja sair? Você pode continuar mais tarde de onde parou.
+ Isso não vai demorar. Estamos configurando sua conta.
+ Isso não vai demorar. Estamos concluindo a ativação.
+ Deixando tudo pronto!
+ Outra carteira
+ Configure um código de 4 dígitos. Ele será usado para pagamentos.
+ Código PIN
+ Criar código PIN
+ O PIN não foi aceito. Tente novamente ou use um código diferente.
+ PIN inválido: evite sequências ou repetições.
+ Você está pronto para começar!
+ Prepare o cartão Tangem e toque para aprovar.
+ Prepare a carteira Tangem
+ Você poderá concluir sua conexão no site de terceiros e retornar ao aplicativo Tangem.
+ Ir para o site
+ Conexão da carteira
+ Escolha a carteira
+ Continuar ativação
+ Vamos continuar configurando sua conta.
+ Bem vindo de volta!
+ Iniciar ativação
+ Siga os passos para configurar sua conta.
+ Bem-vindo!
+ Valor do blockchain
+ Código da moeda
+ Data
+ Código de erro
+ Detalhes da transação
+ Código da categoria do comerciante
+ Cidade comercial
+ Código do país do comerciante
+ Nome do comerciante
+ ID da solicitação
+ Status
+ Transação
+ Valor da transação
+ Hash da transação
+ Solicitação de transação
+ Status da transação
+ Tipo
+ Contestar esta transação
+ Desbloquear
+ Escaneie seu cartão para desbloquear o acesso.
+ Desbloqueio necessário
+ Escolha o seu tipo de carteira
+ Escaneie seu cartão ou anel Tangem para restaurá-lo ou importá-lo de outra carteira.
+ Criar carteira de hardware
+ Deseja comprar uma carteira Tangem?
+ Frase-semente de importação
+ Restaure sua carteira no seu celular ou importe de outro aplicativo — conveniente, mas menos seguro do que um cartão Tangem.
+ O que escolher?
+ A blockchain está inacessível. Tente novamente mais tarde.
+ Escanear Cartão ou Anel
+ Esta carteira já foi ativada anteriormente. Se não foi você quem a ativou, entre em contato com o suporte. \n \n nunca vende carteiras com códigos de acesso pré-gerados.
+ Solicitação para assinar uma mensagem.\n\n%s
+ Aplicativo Dapp %1$s, solicitando que \n assine a transação BNB.\n\n%2$s
+ Ordem de negociação para %1$s\n: %2$s\na receber: %3$s\nValor a pagar: %4$s
+ Detalhes da transação: \n De: %1$s\nPara: %2$s\nValor: %3$s
+ A área de transferência contém o código WalletConnect. Use o valor copiado ou digitalize o código QR.
+ Solicitação para criar transação para %1$s\n%2$s\n\nValor: %3$s\nTaxa \n : %4$s\nTotal: %5$s\nSaldo: %6$s
+ Não foi possível enviar a transação. Fundos insuficientes.
+ Não foi possível estabelecer a sessão WalletConnect. Por favor, tente novamente mais tarde.
+ Nem todos os tokens foram adicionados à sua lista. Adicione-os primeiro e tente novamente. Tokens ausentes:\n
+ Falha ao assinar a mensagem. \n Por favor, tente novamente.
+ Falha ao estabelecer a sessão WalletConnect: erro de tempo limite. Por favor, tente novamente mais tarde.
+ A solicitação de sessão contém blockchains não suportadas para conexão WalletConnect. Blockchains não suportadas:\n
+ Não foi possível estabelecer conexão com este Dapp devido à sua implementação técnica.
+ Ocorreu um erro desconhecido. Mensagem de erro: %sSe o problema persistir, entre em contato com nosso suporte.
+ Cartão ou Anel incorreto selecionado no aplicativo Tangem
+ Falha ao criar transação a partir dos dados do Dapp. Código: %s
+ Ocorreu um erro desconhecido. Código do erro: %dSe o problema persistir, entre em contato com nosso suporte.
+ Transação em várias partes
+ Para que sua transação seja processada com sucesso, ela será dividida em várias partes. Você precisará aproximar seu cartão várias vezes para concluí-la.
+ Nenhuma sessão WalletConnect aberta
+ Ooops. Não há sessões.
+ Falha ao emparelhar a sessão WalletConnect: %1$s
+ Colar da área de transferência
+ Mensagem para %1$s:\n%2$s
+ Solicitação para iniciar uma sessão para\n%1$s\n\nREDE: %2$s\n\nURL: %3$s
+ A operação não pôde ser concluída. \n \n Você já estabeleceu uma sessão WalletConnect com esses parâmetros.
+ Escaneie o novo código
+ Este cartão não pode ser usado para estabelecer uma sessão WalletConnect.
+ Esta rede não é compatível. Selecione outra rede.
+ Selecione a rede
+ A transação está sendo processada. Por favor, aproxime seu cartão várias vezes para concluí-la.
+ Transação em andamento
+ Sessões WalletConnect
+ Conectar-se a dApps
+ WalletConnect
+ A conexão pode levar alguns segundos.
+ Crie uma carteira Tangem
+ De %s
+ Adquira a Tangem Wallet — um dispositivo físico que armazena sua chave privada de forma segura, mesmo offline.
+ Carteira de Hardware
+ Crie uma carteira segura no seu celular em segundos.
+ Carteira Móvel
+ O que escolher
+ Já utiliza uma carteira Tangem?
+ Escaneie agora
+ Escolha um método de configuração de carteira
+ Deseja comprar uma carteira Tangem?
+ Comprar agora
+ Recupere sua carteira existente por meio do backup do Google Drive.
+ Importar do Google Drive
+ Adicionar carteira existente
+ Dispositivos físicos que armazenam sua chave privada de forma segura offline.
+ Escaneie uma carteira Tangem
+ Importe uma carteira existente com sua frase de recuperação.
+ Importar carteira
+ Digite a frase de recuperação
+ Você importou sua carteira com sucesso.
+ Importar carteira
+ Importação concluída
+ Importar carteira
+ %s Preço de mercado
+ últimas 24 horas
+ %s rede
+ O endereço foi copiado para a área de transferência.
+ Sem conexão com a internet
+ Adquira já com 10% desligado
+ Acesse mais de 13.000 criptomoedas. Compre, venda, troque e faça staking com um único toque. \n Vincule até três cartões para backup.
+ Descubra a carteira Tangem
+ Este código de acesso protege sua carteira e é usado para fazer login e assinar transações.
+ Definir/Alterar código de acesso
+ Alterar código de acesso
+ Receba notificações push para transações recebidas na sua carteira.
+ As notificações push podem não estar funcionando corretamente em dispositivos Huawei. Estamos trabalhando ativamente em uma solução e lançaremos uma correção em uma atualização futura. Agradecemos a sua compreensão!
+ Notificações de transação
+ Definir código de acesso
+ Configurações da carteira
+ Tangem
+ Usar %s ou escaneie um cartão/anel para desbloquear o acesso à sua carteira.
+ O processo de concessão de licenças está em andamento e será concluído em breve.
+ Aprovação em andamento
+ A ativação não foi concluída com sucesso. Isso pode ser devido a um problema com a tecnologia NFC ou a uma aproximação incorreta. Entre em contato com nossa equipe de suporte para obter ajuda.
+ Erro de ativação
+ Em 3 de dezembro de 2024, a rede BEP-2 foi desativada por decisão dos desenvolvedores e deixou de receber suporte.
+ A cadeia de beacons BNB foi desativada.
+ Por favor, deposite algum %1$s para cobrir a taxa de rede
+ Fundos insuficientes para cobrir a taxa de rede.
+ Poderia ser melhor
+ Gostei
+ Ok, entendi!
+ Muito legal!
+ Atualizar
+ Iniciar migração
+ Cópia
+ Para manter o acesso aos seus fundos, inicie a migração de acordo com as diretrizes oficiais da Clore.
+ A assinatura de mensagens não é suportada nesta rede.
+ Não foi possível assinar a mensagem. Tente novamente.
+ De acordo com a documentação oficial da Clore, todas as moedas recebidas antes de 21 de dezembro serão migradas para a Clore (token ERC-20); as moedas recebidas após essa data não serão. Uma solução para a transferência está a caminho — fique atento.
+ Mensagem
+ Portal de Reclamações Aberto
+ Para continuar usando seus tokens Clore, você deve concluir a migração de tokens de acordo com as informações no Portal de Reivindicações.
+ Migração de Rede Clore
+ Assinar
+ Assinatura
+ Migração de Rede Clore
+ Você está atualmente no modo de demonstração.
+ Modo de demonstração ativo
+ O cartão que você escaneou é um cartão de desenvolvedor. Não o utilize para criar sua carteira.
+ Não destinado a usuários!
+ %1$s A rede exige um Depósito Existencial. Se sua conta ficar abaixo de %2$s, será desativado e quaisquer fundos restantes serão destruídos.
+ A rede exige depósito existencial.
+ A opção de troca estará disponível após o %s transação concluída
+ Você tem uma transação ativa.
+ A aprovação da troca está em andamento e será concluída em breve.
+ Aprovação em andamento
+ O valor mínimo de troca é %1$sCertifique-se de que o saldo restante após a troca não seja inferior a %2$s.
+ Você não possui nenhum token em seu portfólio que %s Pode ser trocado. Adicione outro token para habilitar a troca.
+ Nenhum token compatível adicionado
+ Para efetuar uma transação, você precisa depositar algum valor. %1$s %2$s
+ Não foi possível cobrir %s taxa
+ O valor a receber deve ser de pelo menos %s
+ Isso pode ocorrer porque o provedor não consegue, no momento, realizar a troca do par selecionado. Aguarde um momento e tente novamente. (Código) %s)
+ O par selecionado está temporariamente indisponível.
+ Alguns fornecedores não são autorizados pela Autoridade de Conduta Financeira do Reino Unido. Você deve evitar negociar com eles.
+ Lista de advertências da FCA
+ Serviço temporariamente indisponível
+ A quantidade de tokens a serem trocados não deve exceder %s
+ O valor a ser trocado deve ser de pelo menos %s
+ A troca não está disponível para este par. Selecione um token diferente e tente novamente.
+ Par de troca não suportado
+ Por favor, altere o valor para troca.
+ Este cartão pode ser uma amostra de produção ou uma falsificação.
+ A verificação de autenticidade falhou.
+ Associado
+ Este token precisa ser associado à sua conta Hedera antes que você possa recebê-lo. Taxa de associação ~%1$s %2$s
+ Este token precisa ser associado à sua conta Hedera antes que você possa recebê-lo.
+ Associe seu token
+ Não é suficiente %sRecarregue sua conta Hedera para associar este token.
+ Tem certeza de que deseja cancelar a transação? Você não poderá tentar realizá-la novamente.
+ Sua transação com um valor de %1$s %2$s Não foi concluído. Você pode tentar novamente para concluí-lo.
+ Você tem uma transação pendente.
+ A última atualização foi %s
+ Apenas %s Assinaturas são deixadas neste cartão. Você deve sacar todos os seus fundos.
+ Baixa contagem de assinaturas
+ Tokens em redes diferentes podem ter endereços diferentes. Verifique se o seu endereço corresponde à rede ao transferir fundos.
+ O MATIC está sendo migrado para o POL. No entanto, não há prazo definido e o MATIC ainda não foi descontinuado. Você pode continuar usando o token MATIC com segurança ou trocá-lo pelo POL.
+ Migração de MATIC para POL
+
+ - Use seu Cartão ou Anel para obter o endereço de uma rede.
+ - Use seu Cartão ou Anel para obter endereços de rede.
+
+ Alguns endereços estão faltando.
+ A rede está inacessível no momento. Tente novamente mais tarde.
+ A rede está inacessível.
+ Recarregue sua carteira
+ Sua carteira ainda não possui backup. Faça um backup agora para proteger seus ativos.
+ Backup ausente
+ Este cartão já foi usado anteriormente para transações. Se o recebeu de uma fonte não confiável, considere retirar todos os fundos. Se o cartão for seu, nenhuma ação é necessária.
+ O cartão já registrou transações.
+ Sua avaliação nos motiva a aprimorar ainda mais a Tangem Wallet.
+ Gostando de Tangem?
+ Você precisa associar seu token antes de receber tokens.
+ Você precisa abrir uma conta Trustline para o seu token antes de recebê-lo.
+ Taxa de aluguel de rede necessária
+ Ação necessária
+ Você entrou em contato com o suporte pelo aplicativo em até 7 dias após a criação da sua carteira? Se sim, ou se não tiver certeza, clique em \"Sim\" e siga as instruções.
+ Obrigado! Tudo pronto! Nenhuma ação adicional é necessária.
+ Você será redirecionado para o site oficial da Tangem. Leia e siga as instruções ali presentes.
+ Você já entrou em contato diretamente com a equipe de suporte da Tangem por meio deste aplicativo?
+ Atualização de segurança obrigatória
+ %1$s é uma vantagem no %2$s rede. Para criar uma %3$s Para realizar a transação, você precisa depositar algum valor. %4$s (%5$s) para cobrir a taxa de rede.
+ Insuficiente %1$s para cobrir a taxa de rede
+ A rede Solana está congestionada. Se a sua transação não for processada em 2 minutos, por favor, repita-a.
+ Alerta da Rede Solana
+ A rede Solana cobra um aluguel de %1$s A cada 2 dias. Contas que não conseguem pagar o aluguel são excluídas da rede. Deposite em sua conta mais do que %2$s para usá-lo gratuitamente.
+ Deslize para baixo para atualizar ou tente novamente mais tarde.
+ Algumas redes estão inacessíveis.
+ Alguns saldos de tokens não puderam ser atualizados.
+ Não é suficiente %sRecarregue sua conta XLM para abrir uma conta Trustline.
+ Este é um cartão de teste (Testnet). Ele não pode processar transações e deve ser usado apenas para fins de teste e desenvolvimento.
+ Apenas para fins de teste
+ O saldo pode estar desatualizado. Atualize a página.
+ Não é suficiente %1$sRecarregue seu %2$s conta para associar este token
+ Ativar Trustline
+ É necessário habilitar uma Trustline para receber este token. A rede requer um %1$s %2$s reserva.
+ Linha de confiança obrigatória
+ A rede necessária %s Não foi adicionado ao seu portfólio. Adicione-o primeiro e depois prossiga com a conexão.
+ Adicionar rede ao portfólio
+ Domínio malicioso
+ Domínio desconhecido
+ Conecte-se mesmo assim
+ Erro de tempo limite. Por favor, tente novamente mais tarde.
+ Falha ao estabelecer conexão com a carteira (WalletConnect).
+ Este domínio não pôde ser verificado. Analise cuidadosamente a solicitação antes de aprová-la.
+ Para continuar, reconecte sua sessão do dApp à rede necessária. %s.
+ Rede não conectada
+ Verifique sua conexão de rede.
+ Tempo limite da solicitação
+ Por favor, volte ao seu navegador e reconecte-se através do WalletConnect.
+ A sessão Wallet Connect foi desconectada.
+ Assine mesmo assim
+ Código de erro: %sSe o problema persistir, entre em contato com nosso suporte.
+ Se o problema persistir, entre em contato com nosso suporte.
+ Ocorreu um erro desconhecido.
+ A carteira Tangem atualmente não oferece suporte. %s.
+ dApp não suportado
+ Código de erro: 8005. Se o problema persistir, entre em contato com nosso suporte.
+ Ocorreu um erro desconhecido.
+ Esta rede %s Não é compatível com a Tangem Wallet e não pode ser conectado.
+ Rede não suportada
+ Atualmente, a Tangem não oferece suporte a uma rede necessária. %s.
+ Redes não suportadas
+ Este domínio passou por verificações e é considerado seguro, confiável e livre de ameaças conhecidas ou atividades suspeitas.
+ Domínio verificado
+ Cartão ou Anel incorreto selecionado no aplicativo.
+ Temos algum tipo de problema.
+ Todos os dApps desconectados
+ Permitir gastar
+ Ao aprovar, você permite que o dApp ou contrato inteligente use tokens em transações futuras.
+ Endereço
+ Conectar
+ Carregando
+ Rede
+ Redes
+ Ilimitado
+ Carteira
+ Aplicativo Conectado
+ Redes conectadas
+ Conectado a %1$s
+ Veja o saldo e a atividade da sua carteira.
+ Assinar transações sem o seu aviso prévio.
+ Solicitar aprovação para transações
+ Não será possível
+ Gostaria de
+ Solicitação de conexão
+ Conexões
+ Conteúdo
+ Copiar dados
+ alfândega
+ dApp desconectado
+ Desconecte tudo
+ Todas as sessões de dApps serão desconectadas. Sua carteira não estará mais vinculada a nenhum dApp.
+ Desconectar todos os dApps
+ Tente emparelhar novamente com um novo URI.
+ Domínio dApp inválido
+ %s Não especifica nenhuma blockchain — nem obrigatória nem opcional. \n Certifique-se de ter usado o URI correto.
+ Sem redes
+ Por favor, gere um novo URI e tente conectar novamente.
+ A proposta de conexão expirou.
+ Alterações estimadas na carteira
+ Não foi possível simular a transação. Prossiga com cautela.
+ A estimativa não é suportada para %s
+ Sugerido por %s
+ Recarregue seu saldo para cobrir a taxa de rede.
+ Insuficiente %1$s
+ Transação maliciosa
+ Adicione o %s Conecte sua carteira à sua rede para esta carteira.
+ A carteira não requer redes.
+ Nova conexão
+ Conecte sua carteira a diferentes dApps
+ Sem sessões
+ Nenhuma alteração detectada na carteira.
+ Foram detectados riscos potenciais ou comportamentos maliciosos. Conectar ou assinar transações pode resultar em perda de fundos.
+ Risco de segurança conhecido
+ Abra o aplicativo Web3 e selecione a opção WalletConnect.
+ Solicitação de
+ Assine mesmo assim
+ Tipo de assinatura
+ É necessária pelo menos uma rede para a conexão do dApp.
+ Especifique as redes selecionadas
+ Assinatura concluída com sucesso
+ Para
+ Solicitação de transação
+ Solicitação de transação
+ Quantidade ilimitada
+ Garanta que cada tentativa de emparelhamento utilize um URI novo e exclusivo.
+ URI já utilizada
+ WalletConnect
+ Transação suspeita
+ Já possui a carteira Tangem?
+ Milhares de ativos
+ carteira de hardware de alta qualidade
+ Entrega rápida
+ Comece com um toque
+ Sem complicações e seguro
+ Sem frase-semente
+ Fácil de usar
+ Crie uma carteira de hardware com a Tangem. Fina como um cartão bancário, segura como um cofre de banco.
+ Crie ou importe uma carteira de software.
+ Crie ou importe uma carteira de software para o seu celular.
+ Comece com a carteira digital
+ Outros métodos
+ Use uma carteira de hardware Tangem.
+ Saiba mais e compre
+ Descartar
+ Seu backup foi interrompido. Deseja retomá-lo?
+ Sim, retomar
+ Descartar
+ Se você descartar o backup agora, terá que restaurar os dispositivos para as configurações de fábrica para começar tudo de novo.
+ Retomar backup
+ Esta é uma ação irreversível.
+ Faça login com %s
+ Escanear Cartão ou Anel
+ Usar %s ou escaneie um cartão ou anel para acessar o aplicativo.
+ Bem vindo de volta!
+ Não, envie tudo
+ Reduzir por %s XTZ
+ Para evitar pagar uma comissão maior na próxima vez que recarregar sua carteira, reduza o valor em %s XTZ
+ Quando o Modo de Rendimento estiver ativo, todas as recargas futuras para este endereço serão fornecidas à Aave. Você ainda poderá gerenciar seus fundos livremente.
+ Seu %s é fornecido à Aave
+ Fornecimento %1$s %2$s para Aave
+ Aprovar
+ A aprovação do seu token foi revogada. Conceda-o novamente para retomar a funcionalidade do serviço.
+ Aprovação necessária
+ A taxa será deduzida e seus ativos serão devolvidos.
+ Para continuar gerando rendimento, é necessária aprovação.
+ Confirmar aprovação
+ Seus fundos estão atualmente vinculados ao protocolo Aave, mas você pode gerenciá-los a qualquer momento.
+ Seu %s é fornecido à Aave
+ Não foi possível carregar o gráfico...
+ Fornecimento %1$s %2$s para Aave.
+ Desativar o modo de rendimento
+ APY %1$s%%
+ Disponível
+ APY atual
+ Ao recarregar para empréstimo, será cobrada uma taxa de rede não superior a %1$s será deduzido do saldo.
+ A taxa de rede está atualmente muito alta para efetuar empréstimos. Os fundos serão disponibilizados assim que a taxa cair para %1$s ou abaixo.
+ Meus fundos
+ Seu %1$s já foi implantado no Aave e está gerando rendimento. Você detém %2$s Os tokens representam seu saldo e acumulam rendimento automaticamente. Ao adicionar fundos, eles são enviados para a Aave para gerar mais rendimento, após a dedução das taxas.
+ Modo de rendimento
+ Rendimento total
+ Transferências para Aave
+ Explore Aave
+ Esta é a taxa de fornecimento atual em %sO custo real será exibido na aba de ativação.
+ Taxa atual
+ Todo o futuro %s Os créditos serão fornecidos automaticamente à Aave, após a dedução da taxa de transação.
+ Uma taxa de rede aproximada de %1$s (%2$s) será deduzido de cada recarga futura e não excederá o seu %3$s (%4$s) limite.
+ Se as taxas de rede ultrapassarem a taxa máxima, a transação não será concluída até que elas diminuam. Você poderá alterar esse limite posteriormente.
+ Taxa máxima
+ O valor mínimo é calculado com base na tarifa de rede atual, garantindo que não ultrapasse 4.%% do valor da recarga, que é igual ao mínimo %1$s (%2$s).
+ recarga mínima
+ Política de taxas de recarga
+ Tangem também leva 15% Taxa de serviço sobre o rendimento gerado.
+ Seus fundos serão transferidos automaticamente para a Aave assim que as tarifas de rede diminuírem ou seu saldo atingir o valor mínimo exigido.
+ As taxas estão mais altas do que o normal devido à alta atividade do mercado. Você pode prosseguir agora ou verificar novamente mais tarde, quando as taxas estiverem mais baixas.
+ Altas taxas de rede
+ Retornos históricos
+ Habilitar %1$s%% APY sobre o seu saldo
+ A aprovação do seu token no Modo Yield foi revogada. Abra o token para conceder permissão novamente.
+ É necessária a aprovação do token.
+ Verifique sua conexão de rede.
+ Informações sobre tarifas de rede indisponíveis
+ Cada recarga será fornecida automaticamente à Aave.
+ Todos %1$s Os dados da sua conta serão fornecidos automaticamente à Aave.
+ Fornecimento automático para Aave
+ Envie, troque ou venda seus fundos instantaneamente, quando quiser.
+ Sem bloqueios
+ Como funciona?
+ Aave é um protocolo on-chain que oferece mercados de liquidez sem custódia, permitindo que os usuários acumulem rendimentos a taxas variáveis.
+ Descentralizado e autossustentável
+ Ao utilizar este serviço, você concorda com os termos do provedor.%1$s e %2$s
+ Conecte-se ao Aave
+ Habilitar %1$s%% APY\não seu saldo
+ Aave %1$s%% • Taxa de juros variável
+ Taxa de juros variável
+ Aave
+ Média %s
+ Retornos do ano passado
+ A taxa de juros atual é sempre variável e calculada automaticamente pelo contrato inteligente on-chain da Aave, com base na oferta e demanda em tempo real.
+ Desenvolvido por
+ A taxa de juros é variável.
+ Ao fazer um depósito, seus fundos serão automaticamente enviados para a Aave para começar a gerar rendimento. %s Será deduzido o valor para cobrir a taxa de transação.
+ Ativos de fornecimento
+ Seu %s Serão fornecidos à Aave sem fechaduras e permanecerão totalmente acessíveis.
+ Consulte a política de taxas de recarga.
+ Suas próximas recargas serão enviadas automaticamente para a Aave.
+ Todas as suas futuras chegadas %1$s Os depósitos serão enviados automaticamente para a Aave.
+ Ativo
+ Em pausa
+ Desativação do modo Yield
+ Desativar esta opção fará com que seus ativos sejam retirados da Aave e convertidos de volta para... %s na sua carteira e interrompa o acúmulo de rendimento.
+ Uma taxa de rede é cobrada pela blockchain quando você sai do Modo Yield.
+ Desativar o modo Yield
+ Fornecimento
+ Fornecimento APY
+ APY
+ Os juros acumulam-se automaticamente.
+ Os juros acumulam-se automaticamente.
+ Modo de rendimento
+ Ativação do modo de rendimento
+ Modo de rendimento
+ Implementação do contrato do modo de rendimento
+ Modo Yield ativado
+ %1$s fornecido à Aave
+ Modo de rendimento desativado
+ %1$s retirado de Aave
+ Modo de rendimento inicializado
+ Modo de rendimento reativado
+ Fornecimento para Aave
+ %1$s fornecido à Aave
+ Retirar do Aave
+ Automático
+ Adicione alguns %1$s %2$s para cobrir a taxa de rede das transações.
+ Não foi possível cobrir %s taxa
+ O Modo Rendimento não está disponível no momento. Tente novamente mais tarde.
+ Modo de rendimento indisponível
+ Não foi possível carregar o gráfico...
+
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 350f3c2c43..a624ed8ccd 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -223,6 +223,7 @@
Добавить
Добавить в портфель
Добавить токен
+ Добавьте токены
Добавлен
Адрес
Все
@@ -408,6 +409,7 @@
Я понял
Я понимаю, продолжить
Произошла ошибка. Пожалуйста, попробуйте снова.
+ Разблокировать
Недоступно
Завершить стейкинг
Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.
@@ -489,8 +491,9 @@
Фильтровать по
Мои сети
Сети
- Часто используемые
+ Популярные
Нет результата
+ Доступные активы
Привет, команда поддержки, у меня возникла ошибка с кодом: %s
Ошибка WalletConnect
Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком.
@@ -733,18 +736,18 @@
Удалить
Например Bitcoin
Ваш портфель был обновлен
- Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.
+ Выбранный токен недоступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.
Голосовать
Кошелёк не поддерживает более одной сети
О монете
Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель
Этот актив в настоящее время не поддерживается в кошельке
- Этот токен не доступен для данного кошелька
+ Этот токен недоступен для данного кошелька
Добавить
APY %s
Мой портфель
Рынок
- Зарабатывай с Tangem
+ Доход с Tangem
Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька
Потяните вверх или коснитесь поисковой строки, чтобы добавить токен
Данные раздела получены из следующих сетей: %s
@@ -843,6 +846,7 @@
Объем торгов (24ч)
Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке.
Объем торгов (24ч)
+ %s в сумме
Объем
Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка
Добавить токены
@@ -1499,7 +1503,7 @@
Обмен…
Вы получите
Выберите токен
- не доступен
+ недоступен
Будем рады вашей обратной связи
Tangem Pay в режиме beta
Карта заморожена
diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml
index 297d8310dc..14557017ad 100644
--- a/core/res/src/main/res/values-uk-rUA/strings.xml
+++ b/core/res/src/main/res/values-uk-rUA/strings.xml
@@ -223,6 +223,7 @@
Додати
Додати у портфель
Додати токен
+ Додайте токени
Додано
Адреса
Усе
diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..be6b9c60c8
--- /dev/null
+++ b/core/res/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,19 @@
+
+
+ 添加代幣
+ 沒有結果
+ 要發送的代幣
+ 解鎖
+ 您的錢包已同步並準備就緒。\n缺少某些代幣?
+ 錢包導入成功
+ 恢復%d%
+ 天
+ 月
+ 週
+ 年
+ 使用固定利率時,您在置換交易時收到的金額將被鎖定,這可以保護您免受交易過程中價格波動的影響。
+ 固定利率
+ 浮動利率意味著您最終收到的金額可能會根據您開始和完成置換之間的市場情況略有變化。
+ 浮動利率
+ 利率固定
+
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index f40c33cb35..f29adfe6d1 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -217,6 +217,7 @@
Add
Add to portfolio
Add token
+ Add tokens
Added
Address
All
@@ -325,6 +326,7 @@
NFT
No
No address
+ No results
Not Added
Not available
Not now
@@ -383,6 +385,7 @@
To
To %s
Today
+ Token to send
- %d token
- %d tokens
@@ -395,6 +398,7 @@
I understand
I understand, continue
There was an error. Please try again.
+ Unlock
Unreachable
Unstake
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.
@@ -478,7 +482,7 @@
Networks
Mostly used
No results
- Earn
+ Staking & Yield mode
Hi support team, I\'ve encountered an error with code: %s
WalletConnect error
You have used a card or ring from another wallet. Tap the card or ring associated with this wallet
@@ -688,6 +692,9 @@
Tap to scan
Tap to sign
Tap the card or ring
+ Your wallet is synced and ready.\nSome tokens missing?
+ Wallet successfully imported
+ Restoring %d%
You have updated biometrics, scan your card or ring to enter
Your balance should be higher than the fee value to make a transfer
Not enough balance
@@ -699,6 +706,7 @@
Mana level
To begin tracking your crypto assets and transactions, add tokens
Manage tokens
+ Scan QR code to send funds or connect to an app
To access all the networks you need to scan the card
Scan your card or ring
Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s
@@ -733,9 +741,11 @@
APY %s
My portfolio
Market
- Earn with Tangem
+ Staking & Yield mode
To generate addresses for selected networks, you must scan your Tangem Wallet card or ring
To add tokens pull this up or tap the search bar
+ Swipe up to explore the market
+ Find new hidden gems
This section’s data is sourced from the following networks: %s
Unable to load the data…
No data
@@ -798,6 +808,10 @@
Holders
The change in the number of token holders within a specific timeframe
Holders
+ D
+ M
+ W
+ Y
Insights
Links
Liquidity
@@ -825,9 +839,11 @@
Total supply
The maximum number of coins or tokens that can ever exist for a particular cryptocurrency
Total supply
+ 24h
Trading volume (24h)
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
Trading volume (24h)
+ %s in total
Volume
Pull this up or tap the search bar to add tokens directly from the market
Add tokens
@@ -851,6 +867,7 @@
Related tokens
Related news
Stay in the loop
+ Trending score
NFC is not available on your device
About NFT
NFT asset
@@ -1037,7 +1054,7 @@
The purchase amount should be no more than %s
The amount to buy must be at least %s
No available providers for this currency
- Quickest Processing
+ Quickest processing
Pay with
Payment method
Available up to %s
@@ -1087,6 +1104,10 @@
Settings
You have not given access to your camera
Camera access denied
+ Sorry, this QR code could not be recognized.
+ Unrecognized QR Code
+ This network isn\'t supported by any of your added tokens. Add a supported token to send crypto.
+ No supported tokens found
No memo required
%1$s (%2$s) on %3$s network
%1$s on %2$s network
@@ -1201,6 +1222,10 @@
Memo: %s
Invalid Memo
Network fee coverage
+
+ - %d token isn\'t compatible with this address
+ - %d tokens aren\'t compatible with this address
+
Nonce
Unique number for each transaction. Use it to resend or cancel a pending transaction.
Enter nonce…
@@ -1228,6 +1253,11 @@
Transaction limitation
Optional
Please align your QR code with the square to scan it. Ensure you scan %s network address.
+ When you use a fixed rate, the amount you receive is locked in at the time of the swap. This protects you from price changes during the transaction.
+ Fixed rate
+ A floating rate means the final amount you receive may change slightly based on market conditions between the time you start and complete the swap.
+ Floating rate
+ Rate is fixed
Recent
Recipient
Not a valid address
@@ -1271,6 +1301,7 @@
Recipient gets %s
Are you sure you want to cancel the conversion? Your previous data will be cleared.
Remove Conversion
+ Something went wrong. Try again.
Send with swap
Transaction sent
Prepare to scan card or ring you want to set up.
@@ -1437,8 +1468,8 @@
The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.
Exchange more tokens at better rates directly in your wallet.
New Swap Provider Available!
- Looking for something else?\nTry searching or explore another crypto!
- Search for any token, even if it’s not in your list yet.
+ Looking for something else?\nTry searching or explore another crypto!
+ Search for any token, even if it’s not in your list yet.
Use search to find what you need
Feel confident with round-the-clock support to help with any issues
Always Here
@@ -1508,6 +1539,7 @@
Hide KYC from main screen
Add funds
Top-up options
+ Add to Google Wallet
Card Number
Change PIN
The card is fully ready for payments.
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index db9bc30c60..f9b0a562a0 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -49,7 +49,7 @@ dependencies {
implementation(deps.compose.coil)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
- implementation(deps.compose.reorderable)
+ api(deps.compose.reorderable)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)
@@ -61,12 +61,12 @@ dependencies {
api(deps.jodatime)
implementation(deps.timber)
implementation(deps.markdown)
- implementation(deps.haze) {
+ api(deps.haze) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")
}
- implementation(deps.haze.materials) {
+ api(deps.haze.materials) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt
deleted file mode 100644
index 1bdae671ae..0000000000
--- a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.tangem.core.ui.components
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.tangem.core.ui.res.TangemTheme
-
-/**
- * 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 BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
- val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
-
- Box(
- modifier = modifier
- .fillMaxWidth()
- .height(TangemTheme.dimens.size100 + bottomBarHeight)
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(
- Color.Transparent,
- backgroundColor,
- ),
- ),
- ),
- )
-}
-
-/**
- * A composable that draws a fade effect. Used on screens with a list of repeating
- * elements and floating button at the bottom of the screen.
- */
-@Composable
-fun Fade(
- modifier: Modifier = Modifier,
- backgroundColor: Color = TangemTheme.colors.background.secondary,
- height: Dp = 32.dp,
-) {
- Box(
- modifier = modifier
- .fillMaxWidth()
- .height(height)
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(
- Color.Transparent,
- backgroundColor,
- ),
- ),
- ),
- )
-}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt
new file mode 100644
index 0000000000..ea58b6415a
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt
@@ -0,0 +1,145 @@
+package com.tangem.core.ui.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.haze.hazeEffectTangem
+import com.tangem.core.ui.res.TangemTheme
+import dev.chrisbanes.haze.HazeProgressive
+import dev.chrisbanes.haze.HazeStyle
+import dev.chrisbanes.haze.HazeTint
+
+/**
+ * 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 BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
+ val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size100 + bottomBarHeight)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ backgroundColor,
+ ),
+ ),
+ ),
+ )
+}
+
+/**
+ * A composable that draws a fade effect at the right end of the screen. Same as [BottomFade]
+ * but with a horizontal gradient.
+ */
+@Composable
+fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
+ Box(
+ modifier = modifier
+ .fillMaxHeight()
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ Color.Transparent,
+ backgroundColor,
+ ),
+ ),
+ ),
+ )
+}
+
+/**
+ * A composable that draws a fade effect at the bottom of the screen. Same as [BottomFade]
+ * but with a vertical blur.
+ */
+@Composable
+fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
+ val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size100 + bottomBarHeight)
+ .hazeEffectTangem(
+ style = HazeStyle(
+ blurRadius = 20.dp,
+ tint = HazeTint(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ backgroundColor,
+ ),
+ ),
+ ),
+ backgroundColor = Color.Transparent,
+ ),
+ ) {
+ progressive =
+ HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f)
+ },
+ )
+}
+
+/**
+ * A composable that draws a fade effect at the right end of the screen. Same as [HorizontalFade]
+ * but with blur.
+ */
+@Composable
+fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .fillMaxHeight()
+ .hazeEffectTangem(
+ style = HazeStyle(
+ blurRadius = 20.dp,
+ tint = HazeTint(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ Color.Transparent,
+ backgroundColor,
+ ),
+ ),
+ ),
+ backgroundColor = Color.Transparent,
+ ),
+ ) {
+ progressive =
+ HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f)
+ },
+ )
+}
+
+/**
+ * A composable that draws a fade effect. Used on screens with a list of repeating
+ * elements and floating button at the bottom of the screen.
+ */
+@Composable
+fun Fade(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = TangemTheme.colors.background.secondary,
+ height: Dp = 32.dp,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(height)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ backgroundColor,
+ ),
+ ),
+ ),
+ )
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt
index 19f8e5fcbd..f3275d7024 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt
@@ -8,16 +8,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
+import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
+ if (LocalRedesignEnabled.current) {
+ UnableToLoadDataV2(onRetryClick, modifier)
+ } else {
+ UnableToLoadDataV1(onRetryClick, modifier)
+ }
+}
+
+@Composable
+private fun UnableToLoadDataV1(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
@@ -37,11 +50,43 @@ fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
}
}
+@Composable
+private fun UnableToLoadDataV2(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.markets_loading_error_title),
+ style = TangemTheme.typography2.bodyRegular14,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ )
+ TangemButton(
+ buttonUM = TangemButtonUM(
+ text = resourceReference(R.string.try_to_load_data_again_button_title),
+ onClick = onRetryClick,
+ type = TangemButtonType.Secondary,
+ size = TangemButtonSize.X8,
+ shape = TangemButtonShape.Rounded,
+ ),
+ )
+ }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
+@Composable
+private fun PreviewV2() {
+ TangemThemePreviewRedesign {
+ UnableToLoadDataV2(onRetryClick = {})
+ }
+}
+
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
-private fun Preview() {
+private fun PreviewV1() {
TangemThemePreview {
- UnableToLoadData(onRetryClick = {})
+ UnableToLoadDataV1(onRetryClick = {})
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt
index f6f4dd18c0..32bf5c0667 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
@@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.core.ui.test.AppBarWithSearchTestTags
/**
* App bar with close icon and search functionality
@@ -135,7 +137,8 @@ private fun CollapsedSearchView(
contentDescription = null,
modifier = Modifier
.clickable { onExpandedChange(true) }
- .padding(end = TangemTheme.dimens.spacing16),
+ .padding(end = TangemTheme.dimens.spacing16)
+ .testTag(AppBarWithSearchTestTags.SEARCH_ICON),
)
}
}
@@ -210,7 +213,8 @@ private fun ExpandedSearchView(
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester)
- .onFocusChanged { onFocusChange(it.hasFocus) },
+ .onFocusChanged { onFocusChange(it.hasFocus) }
+ .testTag(AppBarWithSearchTestTags.TEXT_FIELD),
placeholder = {
Text(text = placeholderSearchText)
},
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt
new file mode 100644
index 0000000000..4b7090b987
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt
@@ -0,0 +1,70 @@
+@file:Suppress("MagicNumber", "UnnecessaryParentheses")
+package com.tangem.core.ui.components.background
+
+import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import com.tangem.core.ui.shader.TangemShader
+import com.tangem.core.ui.shader.runtime.buildEffect
+import kotlin.math.round
+
+@Composable
+fun Modifier.shaderBackground(
+ shader: TangemShader,
+ speed: Float = 1f,
+ fallback: () -> Brush = {
+ Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
+ },
+): Modifier {
+ val runtimeEffect = remember(shader) { buildEffect(shader) }
+ var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
+ val speedModifier = shader.speedModifier
+
+ val time by if (runtimeEffect.isSupported) {
+ var startMillis = remember(shader) { -1L }
+ produceState(0f, speedModifier) {
+ while (true) {
+ withInfiniteAnimationFrameMillis { frameTimeMillis ->
+ if (startMillis < 0) startMillis = frameTimeMillis
+ value = ((frameTimeMillis - startMillis) / 16.6f) / 10f
+ }
+ }
+ }
+ } else {
+ remember { mutableFloatStateOf(-1f) }
+ }
+
+ return this then Modifier.onGloballyPositioned {
+ size = Size(it.size.width.toFloat(), it.size.height.toFloat())
+ }.drawBehind {
+ runtimeEffect.update(
+ shader = shader,
+ time = (time * speed * speedModifier).round(3),
+ width = size.width,
+ height = size.height,
+ ) // set uniforms for the shaders
+
+ if (runtimeEffect.isReady) {
+ drawRect(brush = runtimeEffect.build())
+ } else {
+ drawRect(brush = fallback())
+ }
+ }
+}
+
+private fun Float.round(decimals: Int): Float {
+ var multiplier = 1.0f
+ repeat(decimals) { multiplier *= 10 }
+ return round(this * multiplier) / multiplier
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt
new file mode 100644
index 0000000000..f4e9988a2b
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt
@@ -0,0 +1,169 @@
+@file:Suppress("MagicNumber")
+package com.tangem.core.ui.components.background.northernlights
+
+import androidx.compose.runtime.Composable
+import android.graphics.BlurMaskFilter
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+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
+
+@Suppress("LongMethod")
+@Composable
+internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
+ val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
+
+ // ── Circle 1 (left) ──────────────────────────────────────────────────────
+ val color1 by transition.animateColor(
+ initialValue = Color(0xFF3355EE),
+ targetValue = Color(0xFF5577FF),
+ animationSpec = infiniteRepeatable(
+ animation = tween(4_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "color1",
+ )
+ val x1 by transition.animateFloat(
+ initialValue = 0.05f,
+ targetValue = 0.28f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(5_500, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "x1",
+ )
+ val y1 by transition.animateFloat(
+ initialValue = 0.0f,
+ targetValue = 0.18f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(6_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "y1",
+ )
+
+ // ── Circle 2 (right) ─────────────────────────────────────────────────────
+ val color2 by transition.animateColor(
+ initialValue = Color(0xFF7733CC),
+ targetValue = Color(0xFF4455EE),
+ animationSpec = infiniteRepeatable(
+ animation = tween(5_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(1_500),
+ ),
+ label = "color2",
+ )
+ val x2 by transition.animateFloat(
+ initialValue = 0.68f,
+ targetValue = 0.92f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(7_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "x2",
+ )
+ val y2 by transition.animateFloat(
+ initialValue = 0.02f,
+ targetValue = 0.20f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(5_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(2_000),
+ ),
+ label = "y2",
+ )
+
+ // ── Oval (center) ────────────────────────────────────────────────────────
+ val ovalColor by transition.animateColor(
+ initialValue = Color(0xFF5533CC),
+ targetValue = Color(0xFF8844EE),
+ animationSpec = infiniteRepeatable(
+ animation = tween(7_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(2_500),
+ ),
+ label = "ovalColor",
+ )
+ // ── Circle 3 (center) ────────────────────────────────────────────────────
+ val color3 by transition.animateColor(
+ initialValue = Color(0xFF9933BB),
+ targetValue = Color(0xFFBB44DD),
+ animationSpec = infiniteRepeatable(
+ animation = tween(6_000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(3_000),
+ ),
+ label = "color3",
+ )
+ val x3 by transition.animateFloat(
+ initialValue = 0.35f,
+ targetValue = 0.58f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(6_500, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(1_000),
+ ),
+ label = "x3",
+ )
+ val y3 by transition.animateFloat(
+ initialValue = 0.0f,
+ targetValue = 0.15f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(4_500, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse,
+ initialStartOffset = StartOffset(500),
+ ),
+ label = "y3",
+ )
+
+ var blurRadiusState by remember { mutableFloatStateOf(0f) }
+ val circlePaint1 = remember { Paint() }
+ val circlePaint2 = remember { Paint() }
+ val circlePaint3 = remember { Paint() }
+ val ovalPaint = remember { Paint() }
+
+ Canvas(modifier = modifier) {
+ val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f)
+ val circleRadius = size.width * 0.52f
+
+ // Update maskFilter only when blur radius changes meaningfully
+ if (blurRadiusState != blurRadius) {
+ blurRadiusState = blurRadius
+ val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
+ circlePaint1.asFrameworkPaint().maskFilter = mf
+ circlePaint2.asFrameworkPaint().maskFilter = mf
+ circlePaint3.asFrameworkPaint().maskFilter = mf
+ ovalPaint.asFrameworkPaint().maskFilter = mf
+ }
+
+ circlePaint1.color = color1.copy(alpha = 0.85f)
+ circlePaint2.color = color2.copy(alpha = 0.85f)
+ circlePaint3.color = color3.copy(alpha = 0.85f)
+ ovalPaint.color = ovalColor.copy(alpha = 0.80f)
+
+ drawIntoCanvas { canvas ->
+ canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1)
+ canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2)
+ canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3)
+
+ val halfW = size.width * 0.68f
+ val halfH = size.width * 0.24f
+ val ovalCx = size.width * 0.50f
+ val ovalCy = 0f
+
+ canvas.drawOval(
+ Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH),
+ ovalPaint,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt
new file mode 100644
index 0000000000..27b3583edc
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt
@@ -0,0 +1,148 @@
+@file:Suppress("MagicNumber")
+
+package com.tangem.core.ui.components.background.northernlights
+
+import android.os.Build
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.LocalPowerSavingState
+import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
+
+/**
+ * Animated northern lights background.
+ * Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
+ */
+@Composable
+fun NorthernLightsBackground(
+ containerColor: Color,
+ modifier: Modifier = Modifier,
+ forceSimpleVersion: Boolean = false,
+) {
+ val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
+ if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
+ NorthernLightsBackgroundWithShader(containerColor, modifier)
+ } else {
+ MovingColorfulBlubsBackground(modifier)
+ }
+}
+
+@Suppress("LongMethod")
+@Composable
+private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) {
+ val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
+
+ // 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),
+ 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
+ },
+ repeatMode = RepeatMode.Restart,
+ ),
+ label = "color1",
+ )
+
+ // ── 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",
+ )
+
+ // ── 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",
+ )
+
+ // ── 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",
+ )
+
+ // 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),
+ containerColor,
+ ),
+ speed = 0.5f,
+ scale = 4f,
+ )
+ }
+ val colorsArray = remember { Array(5) { Color.Unspecified } }
+ colorsArray[0] = color1
+ colorsArray[1] = color2
+ colorsArray[2] = color3
+ colorsArray[3] = color4
+ colorsArray[4] = containerColor
+ shader.updateColors(colorsArray)
+
+ Box(
+ modifier = modifier
+ .background(containerColor)
+ .fillMaxSize()
+ .shaderBackground(shader),
+ )
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
index fb14a4a72e..fa8da4d095 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
@@ -5,13 +5,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt
index 386f268756..19e1491cf4 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt
@@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview
fun TangemPullToRefreshContainer(
config: PullToRefreshConfig,
modifier: Modifier = Modifier,
+ indicatorModifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val state = rememberPullToRefreshState()
@@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer(
modifier = modifier,
indicator = {
Indicator(
- modifier = Modifier.align(Alignment.TopCenter),
+ modifier = indicatorModifier.align(Alignment.TopCenter),
isRefreshing = config.isRefreshing,
state = state,
containerColor = TangemTheme.colors.background.tertiary,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt
index fa6f78a262..899a888c8a 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt
@@ -79,7 +79,7 @@ private fun DropdownMenuContent(
content: @Composable ColumnScope.() -> Unit,
) {
// Menu open/close animation.
- val transition = updateTransition(expandedStates, "DropDownMenu")
+ val transition = rememberTransition(expandedStates, "DropDownMenu")
val scale by transition.animateFloat(
transitionSpec = {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt
index d34949ea34..b80af29a35 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt
@@ -32,15 +32,15 @@ internal fun ProvideHaze(content: @Composable () -> Unit) {
*/
@Composable
fun Modifier.hazeEffectTangem(
+ state: HazeState = LocalHazeState.current,
style: HazeStyle = HazeStyle.Unspecified,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
- val hazeState = LocalHazeState.current
- val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
+ val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value
val rootBackground by LocalRootBackgroundColor.current
- return hazeEffect(hazeState, style) {
+ return hazeEffect(state, style) {
fallbackTint = HazeTint(rootBackground)
if (isGlobalBlurEnabled) {
configure()
@@ -78,5 +78,5 @@ fun Modifier.hazeForegroundEffectTangem(
* Applies a haze source to the [Modifier] using the current global haze state.
*/
@Composable
-fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
- this.hazeSource(LocalHazeState.current, zIndex, key)
\ No newline at end of file
+fun Modifier.hazeSourceTangem(state: HazeState = LocalHazeState.current, zIndex: Float = 0f, key: Any? = null) =
+ this.hazeSource(state, zIndex, key)
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt
index 39cdb7d9ea..1f87b768f3 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt
@@ -1,7 +1,6 @@
package com.tangem.core.ui.components.pager
import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
@@ -22,132 +21,147 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.haze.hazeEffectTangem
+import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import dev.chrisbanes.haze.HazeStyle
import kotlin.math.abs
-import kotlin.math.min
import kotlin.math.roundToInt
-private const val ANIMATION_DURATION = 300
-private const val MAX_VISIBLE_DOTS = 5
+internal const val ANIMATION_DURATION = 300
+internal const val MAX_VISIBLE_DOTS = 5
+internal val SPACING = 4.dp
+internal val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
-
-private val SPACING = 4.dp
private val BACKGROUND_SIZE = DpSize(92.dp, 32.dp)
-
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
-private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
-@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) {
- val totalPages = pagerState.pageCount
- val currentIndex = pagerState.currentPage
+ if (LocalRedesignEnabled.current) {
+ PagerIndicatorV2(pagerState, modifier)
+ } else {
+ PagerIndicatorV1(pagerState, modifier)
+ }
+}
+@Composable
+private fun PagerIndicatorV1(pagerState: PagerState, modifier: Modifier = Modifier) {
+ val colors = PagerIndicatorColors(
+ active = TangemTheme.colors.control.key,
+ inactive = TangemTheme.colors.text.tertiary,
+ overlay = TangemTheme.colors.overlay.secondary,
+ )
+ PagerIndicatorContent(
+ pagerState = pagerState,
+ colors = colors,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun PagerIndicatorV2(pagerState: PagerState, modifier: Modifier = Modifier) {
+ val colors = PagerIndicatorColors(
+ active = TangemTheme.colors2.graphic.neutral.primary,
+ inactive = TangemTheme.colors2.graphic.neutral.tertiary,
+ overlay = TangemTheme.colors2.tabs.backgroundSecondary.copy(alpha = .1f),
+ )
+ PagerIndicatorContent(
+ pagerState = pagerState,
+ colors = colors,
+ modifier = modifier,
+ boxModifier = Modifier.hazeEffectTangem(style = HazeStyle(blurRadius = 22.dp, tint = null)),
+ )
+}
+
+@Composable
+private fun rememberPagerIndicatorAnimationState(pagerState: PagerState): PagerIndicatorAnimationState {
+ val density = LocalDensity.current
+ return remember(pagerState.pageCount, density) {
+ PagerIndicatorAnimationState(pagerState.pageCount, pagerState.currentPage, density)
+ }
+}
+
+@Suppress("LongParameterList")
+private fun calculateDotAlpha(
+ isSliding: Boolean,
+ slideDirection: Int,
+ index: Int,
+ displayLower: Int,
+ displayUpper: Int,
+ fadeProgress: Float,
+): Float {
+ return when {
+ !isSliding -> 1f
+ slideDirection > 0 && index == displayLower -> 1f - fadeProgress
+ slideDirection > 0 && index == displayUpper - 1 -> fadeProgress
+ slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress
+ slideDirection < 0 && index == displayLower -> fadeProgress
+ else -> 1f
+ }
+}
+
+@Composable
+private fun PagerIndicatorContent(
+ pagerState: PagerState,
+ colors: PagerIndicatorColors,
+ modifier: Modifier = Modifier,
+ boxModifier: Modifier = Modifier,
+) {
+ val totalPages = pagerState.pageCount
if (totalPages == 0) return
- val indicatorColor = TangemTheme.colors.control.key
- val overlayColor = TangemTheme.colors.overlay.secondary
- val inactiveIndicatorColor = TangemTheme.colors.text.tertiary
-
- val density = LocalDensity.current
-
- val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
-
- var displayLower by remember { mutableIntStateOf(targetLower) }
- var displayUpper by remember { mutableIntStateOf(targetUpper) }
- var prevTargetLower by remember { mutableIntStateOf(targetLower) }
-
- val slideOffset = remember { Animatable(0f) }
- var isSliding by remember { mutableStateOf(false) }
- var slideDirection by remember { mutableIntStateOf(0) }
- val fadeProgress = remember { Animatable(0f) }
- var fadeJob by remember { mutableStateOf(null) }
+ val animState = rememberPagerIndicatorAnimationState(pagerState)
+ val (targetLower, targetUpper) = getWindowBounds(pagerState.pageCount, pagerState.currentPage)
LaunchedEffect(targetLower) {
- if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
- fadeJob?.cancel()
- slideOffset.stop()
- fadeProgress.stop()
-
- val dir = if (targetLower > prevTargetLower) 1 else -1
- val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
- val halfEdge = edgeDotSize / 2
-
- isSliding = true
- slideDirection = dir
- fadeProgress.snapTo(0f)
-
- if (dir > 0) {
- displayLower = prevTargetLower
- displayUpper = targetUpper
- slideOffset.snapTo(halfEdge)
- } else {
- displayLower = targetLower
- displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
- slideOffset.snapTo(-halfEdge)
- }
-
- prevTargetLower = targetLower
-
- fadeJob = launch {
- fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
- }
- slideOffset.animateTo(
- if (dir > 0) -halfEdge else halfEdge,
- tween(ANIMATION_DURATION),
- )
-
- displayLower = targetLower
- displayUpper = targetUpper
- slideOffset.snapTo(0f)
- isSliding = false
- slideDirection = 0
- }
+ animState.onBoundsChange(this, targetLower, targetUpper)
}
- val visibleIndices = (displayLower until displayUpper).toList()
+
+ val visibleIndices = (animState.displayLower until animState.displayUpper).toList()
Box(
modifier = modifier
.width(BACKGROUND_SIZE.width)
.height(BACKGROUND_SIZE.height)
.background(
- color = overlayColor,
+ color = colors.overlay,
shape = CircleShape,
)
- .clip(CircleShape),
+ .clip(CircleShape)
+ .then(boxModifier),
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier.offset {
- IntOffset(slideOffset.value.roundToInt(), 0)
+ IntOffset(animState.slideOffset.value.roundToInt(), 0)
},
horizontalArrangement = Arrangement.spacedBy(SPACING),
verticalAlignment = Alignment.CenterVertically,
) {
visibleIndices.forEach { index ->
- val dotAlpha = when {
- !isSliding -> 1f
- slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
- slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
- slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
- slideDirection < 0 && index == displayLower -> fadeProgress.value
- else -> 1f
- }
+ val dotAlpha = calculateDotAlpha(
+ isSliding = animState.isSliding,
+ slideDirection = animState.slideDirection,
+ index = index,
+ displayLower = animState.displayLower,
+ displayUpper = animState.displayUpper,
+ fadeProgress = animState.fadeProgress.value,
+ )
key(index) {
Dot(
index = index,
- currentIndex = currentIndex,
+ currentIndex = pagerState.currentPage,
totalPages = totalPages,
- activeColor = indicatorColor,
- inactiveColor = inactiveIndicatorColor,
+ activeColor = colors.active,
+ inactiveColor = colors.inactive,
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
)
}
@@ -156,19 +170,6 @@ fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) {
}
}
-private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair {
- if (totalPages <= MAX_VISIBLE_DOTS) {
- return 0 to totalPages
- }
- val lowerBound = when {
- currentIndex <= 1 -> 0
- currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
- else -> currentIndex - 2
- }
- val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
- return lowerBound to upperBound
-}
-
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
if (index == currentIndex) {
return CURRENT_DOT_SIZE
@@ -180,6 +181,46 @@ private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
return params.calculateSize()
}
+@Composable
+private fun Dot(
+ index: Int,
+ currentIndex: Int,
+ totalPages: Int,
+ activeColor: Color,
+ inactiveColor: Color,
+ modifier: Modifier = Modifier,
+) {
+ val isActive = index == currentIndex
+ val size = getDotSize(index, currentIndex, totalPages)
+
+ val animSpec = tween(ANIMATION_DURATION)
+ val colorSpec = tween(ANIMATION_DURATION)
+
+ val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
+ val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
+ val animatedColor by animateColorAsState(
+ targetValue = if (isActive) activeColor else inactiveColor,
+ animationSpec = colorSpec,
+ label = "c$index",
+ )
+
+ val shape = RoundedCornerShape(animatedHeight / 2)
+
+ Box(
+ modifier = modifier
+ .width(animatedWidth)
+ .height(animatedHeight)
+ .background(animatedColor, shape),
+ )
+}
+
+@Immutable
+private data class PagerIndicatorColors(
+ val active: Color,
+ val inactive: Color,
+ val overlay: Color,
+)
+
private class DotSizeParams private constructor(
val posInWindow: Int,
val currentPosInWindow: Int,
@@ -248,43 +289,27 @@ private class DotSizeParams private constructor(
}
}
+@Preview(showBackground = true)
@Composable
-private fun Dot(
- index: Int,
- currentIndex: Int,
- totalPages: Int,
- activeColor: Color,
- inactiveColor: Color,
- modifier: Modifier = Modifier,
-) {
- val isActive = index == currentIndex
- val size = getDotSize(index, currentIndex, totalPages)
-
- val animSpec = tween(ANIMATION_DURATION)
- val colorSpec = tween(ANIMATION_DURATION)
-
- val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
- val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
- val animatedColor by animateColorAsState(
- targetValue = if (isActive) activeColor else inactiveColor,
- animationSpec = colorSpec,
- label = "c$index",
- )
-
- val shape = RoundedCornerShape(animatedHeight / 2)
-
- Box(
- modifier = modifier
- .width(animatedWidth)
- .height(animatedHeight)
- .background(animatedColor, shape),
- )
+private fun PagerIndicatorPreviewV1() {
+ TangemThemePreview {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ listOf(0, 1, 2, 3, 4).forEach { page ->
+ PagerIndicator(rememberPagerState(page) { 5 })
+ }
+ }
+ }
}
@Preview(showBackground = true)
@Composable
-private fun PagerIndicatorPreview() {
- TangemThemePreview {
+private fun PagerIndicatorPreviewV2() {
+ TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt
new file mode 100644
index 0000000000..1968908ace
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicatorAnimationState.kt
@@ -0,0 +1,95 @@
+package com.tangem.core.ui.components.pager
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.runtime.*
+import androidx.compose.ui.unit.Density
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.math.min
+
+@Stable
+internal class PagerIndicatorAnimationState(
+ private val totalPages: Int,
+ initialCurrentPage: Int,
+ private val density: Density,
+) {
+ var displayLower by mutableIntStateOf(0)
+ private set
+ var displayUpper by mutableIntStateOf(0)
+ private set
+
+ val slideOffset = Animatable(0f)
+ var isSliding by mutableStateOf(false)
+ private set
+ var slideDirection by mutableIntStateOf(0)
+ private set
+ val fadeProgress = Animatable(0f)
+ private var fadeJob by mutableStateOf(null)
+
+ private var prevTargetLower by mutableIntStateOf(0)
+
+ init {
+ val (lower, upper) = getWindowBounds(totalPages, initialCurrentPage)
+ displayLower = lower
+ displayUpper = upper
+ prevTargetLower = lower
+ }
+
+ suspend fun onBoundsChange(scope: CoroutineScope, targetLower: Int, targetUpper: Int) {
+ if (targetLower == prevTargetLower || totalPages <= MAX_VISIBLE_DOTS) {
+ return
+ }
+
+ fadeJob?.cancel()
+ slideOffset.stop()
+ fadeProgress.stop()
+
+ val dir = if (targetLower > prevTargetLower) 1 else -1
+ val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
+ val halfEdge = edgeDotSize / 2
+
+ isSliding = true
+ slideDirection = dir
+ fadeProgress.snapTo(0f)
+
+ if (dir > 0) {
+ displayUpper = targetUpper
+ slideOffset.snapTo(halfEdge)
+ } else {
+ displayLower = targetLower
+ displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
+ slideOffset.snapTo(-halfEdge)
+ }
+
+ prevTargetLower = targetLower
+
+ fadeJob = scope.launch {
+ fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
+ }
+ slideOffset.animateTo(
+ if (dir > 0) -halfEdge else halfEdge,
+ tween(ANIMATION_DURATION),
+ )
+
+ displayLower = targetLower
+ displayUpper = targetUpper
+ slideOffset.snapTo(0f)
+ isSliding = false
+ slideDirection = 0
+ }
+}
+
+internal fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair {
+ if (totalPages <= MAX_VISIBLE_DOTS) {
+ return 0 to totalPages
+ }
+ val lowerBound = when {
+ currentIndex <= 1 -> 0
+ currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
+ else -> currentIndex - 2
+ }
+ val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
+ return lowerBound to upperBound
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt
index 2c05dbfe7d..1d0dbc825f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt
@@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle {
override fun createShader(size: Size): Shader {
val center = Offset(size.width / 2f, size.height / 2f)
val diagonal = sqrt(size.width * size.width + size.height * size.height)
- val direction = Offset(x = 1f, y = 0.5f)
- val halfDist = diagonal / 2f
- val baseStart = center - direction * halfDist
- val baseEnd = center + direction * halfDist
- val shift = direction * offset * diagonal
+ // Subtle diagonal angle, similar to iOS shimmer
+ val direction = Offset(x = 1f, y = 0.3f)
+ // Half-width of the blob (80% of diagonal total — wide, soft sweep)
+ val bandHalf = diagonal * 0.40f
+
+ // Sweep the highlight center from left-of-element to right-of-element.
+ // offset 0..1 maps to a full pass including off-screen padding on both sides.
+ val shift = direction * ((offset - 0.5f) * diagonal * 1.5f)
+ val highlightCenter = center + shift
+
+ // Full color text with a wide, gradual low-alpha dip sweeping left → right
return LinearGradientShader(
- colors = listOf(textColor.copy(alpha = 0.2f), textColor),
- from = baseStart + shift,
- to = baseEnd + shift,
- colorStops = listOf(0.0f, 0.15f),
- tileMode = TileMode.Mirror,
+ colors = listOf(
+ textColor,
+ textColor.copy(alpha = 0.75f),
+ textColor.copy(alpha = 0.45f),
+ textColor.copy(alpha = 0.3f),
+ textColor.copy(alpha = 0.45f),
+ textColor.copy(alpha = 0.75f),
+ textColor,
+ ),
+ from = highlightCenter - direction * bandHalf,
+ to = highlightCenter + direction * bandHalf,
+ colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f),
+ tileMode = TileMode.Clamp,
)
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt
index 7797159dc1..c09e64697b 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt
@@ -5,6 +5,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
/** Tokens list item state */
@Immutable
@@ -45,15 +46,34 @@ sealed interface TokensListItemUM {
val tokenItemUM: TokenItemState,
val isExpanded: Boolean,
val isCollapsable: Boolean,
- val tokens: ImmutableList,
+ val content: PortfolioItemContentUM,
) : TokensListItemUM {
override val id: String = tokenItemUM.id
+
+ val tokens: ImmutableList
+ get() = when (content) {
+ is PortfolioItemContentUM.Tokens -> content.tokens
+ is PortfolioItemContentUM.Empty -> persistentListOf()
+ }
}
data class Text(override val id: Any, val text: TextReference) : TokensListItemUM
}
+@Immutable
sealed interface PortfolioTokensListItemUM {
/** Unique ID */
val id: Any
+}
+
+@Immutable
+sealed interface PortfolioItemContentUM {
+ data class Tokens(val tokens: ImmutableList) : PortfolioItemContentUM
+ data class Empty(val action: Action? = null) : PortfolioItemContentUM {
+
+ data class Action(
+ val text: TextReference,
+ val onClick: () -> Unit,
+ )
+ }
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt
new file mode 100644
index 0000000000..5528190513
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt
@@ -0,0 +1,370 @@
+package com.tangem.core.ui.ds
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+private const val ANIMATION_DURATION = 300
+private const val MAX_VISIBLE_DOTS = 5
+private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
+private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
+private const val MIN_DISTANCE_FOR_HINT_DOT = 2
+
+private val SPACING = 4.dp
+private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
+private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
+private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
+private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
+
+/**
+ * // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation.
+ *
+ * A pager indicator that adapts to the number of pages and the current page index.
+ *
+ * For 5 or fewer pages, it shows all dots with the current page highlighted.
+ * For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position.
+ *
+ * @param pagerState state of the pager to observe
+ * @param activeIndicatorColor color for the active page indicator
+ * @param inactiveIndicatorColor color for the inactive page indicators
+ * @param modifier modifier for styling
+ */
+@Suppress("LongMethod", "CyclomaticComplexMethod")
+@Composable
+fun TangemPagerIndicator(
+ pagerState: PagerState,
+ modifier: Modifier = Modifier,
+ activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary,
+ inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary,
+) {
+ val totalPages = pagerState.pageCount
+ val currentIndex = pagerState.currentPage
+
+ if (totalPages == 0) return
+
+ val density = LocalDensity.current
+
+ val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
+
+ var displayLower by remember { mutableIntStateOf(targetLower) }
+ var displayUpper by remember { mutableIntStateOf(targetUpper) }
+ var prevTargetLower by remember { mutableIntStateOf(targetLower) }
+
+ val slideOffset = remember { Animatable(0f) }
+ var isSliding by remember { mutableStateOf(false) }
+ var slideDirection by remember { mutableIntStateOf(0) }
+ val fadeProgress = remember { Animatable(0f) }
+ var fadeJob by remember { mutableStateOf(null) }
+
+ LaunchedEffect(targetLower) {
+ if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
+ fadeJob?.cancel()
+ slideOffset.stop()
+ fadeProgress.stop()
+
+ val dir = if (targetLower > prevTargetLower) 1 else -1
+ val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
+ val halfEdge = edgeDotSize / 2
+
+ isSliding = true
+ slideDirection = dir
+ fadeProgress.snapTo(0f)
+
+ if (dir > 0) {
+ displayLower = prevTargetLower
+ displayUpper = targetUpper
+ slideOffset.snapTo(halfEdge)
+ } else {
+ displayLower = targetLower
+ displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
+ slideOffset.snapTo(-halfEdge)
+ }
+
+ prevTargetLower = targetLower
+
+ fadeJob = launch {
+ fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
+ }
+ slideOffset.animateTo(
+ if (dir > 0) -halfEdge else halfEdge,
+ tween(ANIMATION_DURATION),
+ )
+
+ displayLower = targetLower
+ displayUpper = targetUpper
+ slideOffset.snapTo(0f)
+ isSliding = false
+ slideDirection = 0
+ }
+ }
+ val visibleIndices = (displayLower until displayUpper).toList()
+
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+ ) {
+ Row(
+ modifier = Modifier.offset {
+ IntOffset(slideOffset.value.roundToInt(), 0)
+ },
+ horizontalArrangement = Arrangement.spacedBy(SPACING),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ visibleIndices.forEach { index ->
+ val dotAlpha = when {
+ !isSliding -> 1f
+ slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
+ slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
+ slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
+ slideDirection < 0 && index == displayLower -> fadeProgress.value
+ else -> 1f
+ }
+
+ key(index) {
+ Dot(
+ index = index,
+ currentIndex = currentIndex,
+ totalPages = totalPages,
+ activeColor = activeIndicatorColor,
+ inactiveColor = inactiveIndicatorColor,
+ modifier = Modifier.graphicsLayer { alpha = dotAlpha },
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair {
+ if (totalPages <= MAX_VISIBLE_DOTS) {
+ return 0 to totalPages
+ }
+ val lowerBound = when {
+ currentIndex <= 1 -> 0
+ currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
+ else -> currentIndex - 2
+ }
+ val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
+ return lowerBound to upperBound
+}
+
+private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
+ if (index == currentIndex) {
+ return CURRENT_DOT_SIZE
+ }
+ if (totalPages <= MAX_VISIBLE_DOTS) {
+ return NORMAL_DOT_SIZE
+ }
+ val params = DotSizeParams.create(index, currentIndex, totalPages)
+ return params.calculateSize()
+}
+
+private class DotSizeParams private constructor(
+ val posInWindow: Int,
+ val currentPosInWindow: Int,
+ val hiddenLeft: Int,
+ val hiddenRight: Int,
+ val distanceFromCurrent: Int,
+) {
+ private val lastPos = MAX_VISIBLE_DOTS - 1
+ private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
+
+ fun calculateSize(): DpSize = when {
+ isCentered -> getCenteredSize()
+ hiddenRight >= 1 -> getRightEdgeSize()
+ hiddenLeft >= 1 -> getLeftEdgeSize()
+ else -> NORMAL_DOT_SIZE
+ }
+
+ private fun getCenteredSize(): DpSize = when (posInWindow) {
+ 0, lastPos -> HINT_DOT_SIZE
+ else -> NORMAL_DOT_SIZE
+ }
+
+ private fun getRightEdgeSize(): DpSize {
+ val isLastPos = posInWindow == lastPos
+ val isSecondToLast = posInWindow == lastPos - 1
+ val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
+ val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
+ val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
+
+ return when {
+ isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
+ isLastPos && isModerateDistance -> HINT_DOT_SIZE
+ isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
+ else -> NORMAL_DOT_SIZE
+ }
+ }
+
+ private fun getLeftEdgeSize(): DpSize {
+ val isFirstPos = posInWindow == 0
+ val isSecondPos = posInWindow == 1
+ val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
+ val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
+ val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
+
+ return when {
+ isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
+ isFirstPos && isModerateDistance -> HINT_DOT_SIZE
+ isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
+ else -> NORMAL_DOT_SIZE
+ }
+ }
+
+ companion object {
+ fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
+ val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
+ val posInWindow = index - windowStart
+ val currentPosInWindow = currentIndex - windowStart
+ return DotSizeParams(
+ posInWindow = posInWindow,
+ currentPosInWindow = currentPosInWindow,
+ hiddenLeft = windowStart,
+ hiddenRight = totalPages - windowEnd,
+ distanceFromCurrent = abs(posInWindow - currentPosInWindow),
+ )
+ }
+ }
+}
+
+@Composable
+private fun Dot(
+ index: Int,
+ currentIndex: Int,
+ totalPages: Int,
+ activeColor: Color,
+ inactiveColor: Color,
+ modifier: Modifier = Modifier,
+) {
+ val isActive = index == currentIndex
+ val size = getDotSize(index, currentIndex, totalPages)
+
+ val animSpec = tween(ANIMATION_DURATION)
+ val colorSpec = tween(ANIMATION_DURATION)
+
+ val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
+ val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
+ val animatedColor by animateColorAsState(
+ targetValue = if (isActive) activeColor else inactiveColor,
+ animationSpec = colorSpec,
+ label = "c$index",
+ )
+
+ val shape = RoundedCornerShape(animatedHeight / 2)
+
+ Box(
+ modifier = modifier
+ .width(animatedWidth)
+ .height(animatedHeight)
+ .background(animatedColor, shape),
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PagerIndicatorPreview() {
+ TangemThemePreviewRedesign {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ listOf(0, 1, 2, 3, 4).forEach { page ->
+ TangemPagerIndicator(rememberPagerState(page) { 5 })
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PagerIndicator6ItemsPreview() {
+ TangemThemePreviewRedesign {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ listOf(0, 1, 2, 3, 4, 5).forEach { page ->
+ TangemPagerIndicator(rememberPagerState(page) { 6 })
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PagerIndicator7ItemsPreview() {
+ TangemThemePreviewRedesign {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
+ TangemPagerIndicator(rememberPagerState(page) { 7 })
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PagerIndicator10ItemsPreview() {
+ TangemThemePreviewRedesign {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
+ TangemPagerIndicator(rememberPagerState(page) { 10 })
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PagerIndicatorSmallCountsPreview() {
+ TangemThemePreviewRedesign {
+ Column(
+ Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ TangemPagerIndicator(rememberPagerState(0) { 1 })
+ TangemPagerIndicator(rememberPagerState(1) { 2 })
+ TangemPagerIndicator(rememberPagerState(1) { 3 })
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
index 7f9f95b4d3..b8939058d7 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
@@ -1,13 +1,11 @@
package com.tangem.core.ui.ds.badge
import android.content.res.Configuration
-import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
@@ -15,18 +13,17 @@ 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.graphics.Shape
-import androidx.compose.ui.res.painterResource
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 com.tangem.core.ui.R
import com.tangem.core.ui.ds.badge.TangemBadgeSize.*
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.clickableSingle
-import com.tangem.core.ui.extensions.resolveReference
-import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@@ -43,7 +40,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
TangemBadge(
text = badgeUM.text,
- iconRes = badgeUM.iconRes,
+ tangemIconUM = badgeUM.tangemIconUM,
size = badgeUM.size,
shape = badgeUM.shape,
color = badgeUM.color,
@@ -60,7 +57,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
*
* @param text TextReference for the badge label.
* @param modifier Modifier to be applied to the badge.
- * @param iconRes Drawable resource ID for the icon to be displayed in the badge.
+ * @param tangemIconUM Model of representation for the icon to be displayed in the badge.
* @param size [TangemBadgeSize] defining the size of the badge.
* @param shape [TangemBadgeShape] defining the shape of the badge.
* @param color [TangemBadgeColor] defining the color scheme of the badge.
@@ -72,14 +69,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
*/
@Composable
fun TangemBadge(
- text: TextReference,
modifier: Modifier = Modifier,
- @DrawableRes iconRes: Int? = null,
+ text: TextReference? = null,
+ tangemIconUM: TangemIconUM? = null,
size: TangemBadgeSize = X9,
shape: TangemBadgeShape = TangemBadgeShape.Default,
color: TangemBadgeColor = TangemBadgeColor.Gray,
type: TangemBadgeType = TangemBadgeType.Solid,
- iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
+ iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
onClick: (() -> Unit)? = null,
) {
val iconColor = getIconColor(type = type, color = color)
@@ -93,37 +90,84 @@ fun TangemBadge(
.padding(size.toPaddingDp(position = iconPosition))
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
) {
- AnimatedVisibility(
- visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
- modifier = Modifier.size(size = size.toContentSize()),
- label = "Start Icon Visibility",
- ) {
- val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
- Icon(
- painter = painterResource(id = wrappedIconRes),
- contentDescription = null,
- tint = iconColor,
- )
- }
- Text(
- text = text.resolveReference(),
- style = size.toTextStyle(),
- maxLines = 1,
- color = getTextColor(type = type, color = color),
+ StartIcon(
+ tangemIconUM = tangemIconUM,
+ iconPosition = iconPosition,
+ size = size,
+ iconColor = iconColor,
)
-
AnimatedVisibility(
- visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
- modifier = Modifier.size(size = size.toContentSize()),
- label = "End Icon Visibility",
+ visible = text != null,
+ label = "Text Visibility",
) {
- val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
- Icon(
- painter = painterResource(id = wrappedIconRes),
- contentDescription = null,
- tint = iconColor,
+ val wrappedText = remember(this) { requireNotNull(text) }
+ Text(
+ text = wrappedText.resolveReference(),
+ style = size.toTextStyle(),
+ maxLines = 1,
+ color = getTextColor(type = type, color = color),
)
}
+ EndIcon(
+ tangemIconUM = tangemIconUM,
+ iconPosition = iconPosition,
+ size = size,
+ iconColor = iconColor,
+ )
+ }
+}
+
+@Composable
+private fun StartIcon(
+ iconPosition: TangemBadgeIconPosition,
+ size: TangemBadgeSize,
+ iconColor: Color,
+ tangemIconUM: TangemIconUM? = null,
+) {
+ AnimatedVisibility(
+ visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End,
+ modifier = Modifier.size(size = size.toContentSize()),
+ label = "Start Icon Visibility",
+ ) {
+ val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) }
+ TangemIcon(
+ modifier = Modifier.fillMaxSize(),
+ tangemIconUM = when (wrappedIconRes) {
+ is TangemIconUM.Currency,
+ is TangemIconUM.Ident,
+ is TangemIconUM.Image,
+ is TangemIconUM.Url,
+ -> wrappedIconRes
+ is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
+ },
+ )
+ }
+}
+
+@Composable
+private fun EndIcon(
+ iconPosition: TangemBadgeIconPosition,
+ size: TangemBadgeSize,
+ iconColor: Color,
+ tangemIconUM: TangemIconUM? = null,
+) {
+ AnimatedVisibility(
+ visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End,
+ modifier = Modifier.size(size = size.toContentSize()),
+ label = "End Icon Visibility",
+ ) {
+ val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) }
+ TangemIcon(
+ modifier = Modifier.fillMaxSize(),
+ tangemIconUM = when (wrappedIconRes) {
+ is TangemIconUM.Currency,
+ is TangemIconUM.Ident,
+ is TangemIconUM.Image,
+ is TangemIconUM.Url,
+ -> wrappedIconRes
+ is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
+ },
+ )
}
}
@@ -178,14 +222,17 @@ enum class TangemBadgeSize {
X4 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
+ TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp)
}
X6 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
+ TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp)
}
X9 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
+ TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp)
}
}
@@ -222,6 +269,7 @@ enum class TangemBadgeSize {
enum class TangemBadgeIconPosition {
Start,
End,
+ None,
}
/**
@@ -240,6 +288,7 @@ enum class TangemBadgeColor {
Blue,
Red,
Gray,
+ Green,
}
@ReadOnlyComposable
@@ -258,6 +307,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when
-> TangemTheme.colors2.markers.iconRed
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
}
+ TangemBadgeColor.Green -> when (type) {
+ TangemBadgeType.Outline,
+ TangemBadgeType.Tinted,
+ -> TangemTheme.colors2.markers.iconGreen
+ TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
+ }
}
@ReadOnlyComposable
@@ -276,8 +331,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when
-> TangemTheme.colors2.markers.textRed
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
+ TangemBadgeColor.Green -> when (type) {
+ TangemBadgeType.Outline,
+ TangemBadgeType.Tinted,
+ -> TangemTheme.colors2.markers.textGreen
+ TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
+ }
}
+@Suppress("CyclomaticComplexMethod")
@ReadOnlyComposable
@Composable
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
@@ -286,6 +348,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
+ TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen
},
)
TangemBadgeType.Tinted -> background(
@@ -293,6 +356,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
+ TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen
},
)
TangemBadgeType.Outline -> {
@@ -301,6 +365,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
+ TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen
},
shape = shape,
width = 1.dp,
@@ -320,16 +385,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
- repeat(2) { yIndex ->
+ repeat(3) { yIndex ->
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
repeat(TangemBadgeType.entries.size) { index ->
TangemBadge(
- text = stringReference("Title"),
- iconRes = R.drawable.ic_information_24,
+ text = stringReference("Title").takeIf { yIndex < 2 },
+ tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24),
type = TangemBadgeType.entries[index],
color = params,
shape = TangemBadgeShape.entries[yIndex % 2],
- iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
+ iconPosition = TangemBadgeIconPosition.entries[yIndex],
)
}
}
@@ -344,6 +409,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider TangemTheme.colors2.text.status.disabled
@@ -70,7 +73,8 @@ fun GhostTangemButton(
}
TangemButtonInternal(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier
+ .clip(shape = shape.toShape(size)),
text = text,
contentColor = contentColor,
enabled = enabled,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt
index 4d6f5dc9d9..b9426982dd 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt
@@ -7,10 +7,10 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -60,72 +60,91 @@ internal fun TangemButtonInternal(
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
) {
- Row(
- modifier = modifier
- .testTag(BaseButtonTestTags.BUTTON)
- .height(size.toHeightDp())
- .conditionalCompose(text == null) {
- width(size.toHeightDp())
+ ProvideButtonRippleConfiguration {
+ Row(
+ modifier = modifier
+ .testTag(BaseButtonTestTags.BUTTON)
+ .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
+ .height(size.toHeightDp())
+ .conditionalCompose(text == null) {
+ width(size.toHeightDp())
+ }
+ .conditionalCompose(text != null) {
+ padding(horizontal = size.toPaddingDp())
+ }
+ .animateContentSize(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AnimatedVisibility(
+ visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
+ modifier = Modifier.size(size = size.toContentSize()),
+ ) {
+ val wrappedIconRes = remember(this, iconRes) { requireNotNull(iconRes) }
+ TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
}
- .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
- .conditionalCompose(text != null) {
- padding(horizontal = size.toPaddingDp())
+
+ AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
+ val wrappedText = remember(this) { requireNotNull(text) }
+ val textStyle = size.toTextStyle()
+ Text(
+ text = wrappedText.resolveReference(),
+ style = textStyle,
+ color = contentColor,
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = TextAutoSize.StepBased(
+ minFontSize = 12.sp,
+ maxFontSize = textStyle.fontSize,
+ ),
+ modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
+ )
}
- .animateContentSize(),
- horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
- verticalAlignment = Alignment.CenterVertically,
+
+ AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
+ val wrappedText = remember(this) { requireNotNull(descriptionText) }
+ val textStyle = TangemTheme.typography2.captionSemibold12
+ Text(
+ text = wrappedText.resolveReference(),
+ style = textStyle,
+ color = TangemTheme.colors2.text.status.disabled,
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = TextAutoSize.StepBased(
+ minFontSize = 12.sp,
+ maxFontSize = textStyle.fontSize,
+ ),
+ modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
+ )
+ }
+
+ AnimatedVisibility(
+ visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
+ modifier = Modifier.size(size = size.toContentSize()),
+ ) {
+ val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
+ TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
+ }
+ }
+ }
+}
+
+@Composable
+private inline fun ProvideButtonRippleConfiguration(crossinline content: @Composable () -> Unit) {
+ CompositionLocalProvider(
+ LocalRippleConfiguration provides RippleConfiguration(
+ color = TangemTheme.colors2.overlay.overlaySecondary,
+ RippleAlpha(
+ pressedAlpha = 0.4f,
+ focusedAlpha = 0.4f,
+ draggedAlpha = 0.4f,
+ hoveredAlpha = 0.4f,
+ ),
+ ),
) {
- AnimatedVisibility(
- visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
- modifier = Modifier.size(size = size.toContentSize()),
- ) {
- val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
- TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
- }
-
- AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
- val wrappedText = remember(this) { requireNotNull(text) }
- val textStyle = size.toTextStyle()
- Text(
- text = wrappedText.resolveReference(),
- style = textStyle,
- color = contentColor,
- textAlign = TextAlign.Center,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- autoSize = TextAutoSize.StepBased(
- minFontSize = 12.sp,
- maxFontSize = textStyle.fontSize,
- ),
- modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
- )
- }
-
- AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
- val wrappedText = remember(this) { requireNotNull(descriptionText) }
- val textStyle = TangemTheme.typography2.captionSemibold12
- Text(
- text = wrappedText.resolveReference(),
- style = textStyle,
- color = TangemTheme.colors2.text.status.disabled,
- textAlign = TextAlign.Center,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- autoSize = TextAutoSize.StepBased(
- minFontSize = 12.sp,
- maxFontSize = textStyle.fontSize,
- ),
- modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
- )
- }
-
- AnimatedVisibility(
- visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
- modifier = Modifier.size(size = size.toContentSize()),
- ) {
- val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
- TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
- }
+ content()
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt
index 0b0e426629..a3bb16c4da 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt
@@ -1,6 +1,7 @@
package com.tangem.core.ui.ds.button
import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.TextReference
/**
@@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference
*
[REDACTED_AUTHOR]
*/
+@Stable
data class TangemButtonUM(
val text: TextReference? = null,
val descriptionText: TextReference? = null,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt
index 9abdda6653..f1bf0d3fdb 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt
@@ -48,9 +48,9 @@ fun TangemCheckbox(
isEnabled: Boolean = true,
) {
val shape = if (isRounded) {
- RoundedCornerShape(TangemTheme.dimens2.x1)
- } else {
CircleShape
+ } else {
+ RoundedCornerShape(TangemTheme.dimens2.x1)
}
Box(
modifier = modifier
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt
new file mode 100644
index 0000000000..69a475c2a8
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt
@@ -0,0 +1,288 @@
+package com.tangem.core.ui.ds.contextmenu
+
+import android.content.res.Configuration
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.*
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import com.tangem.core.ui.components.haze.hazeEffectTangem
+import com.tangem.core.ui.components.haze.hazeSourceTangem
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import com.tangem.core.ui.test.PopUpMenuTestTags
+import dev.chrisbanes.haze.rememberHazeState
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
+ */
+@Composable
+fun TangemContextMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset.Zero,
+ properties: PopupProperties = PopupProperties(focusable = true),
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ val expandedStates = remember { MutableTransitionState(false) }
+ expandedStates.targetState = expanded
+
+ if (expandedStates.currentState || expandedStates.targetState) {
+ val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
+ val density = LocalDensity.current
+ val popupPositionProvider = DropdownMenuPositionProvider(
+ offset,
+ density,
+ ) { parentBounds, menuBounds ->
+ transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
+ }
+
+ Popup(
+ onDismissRequest = onDismissRequest,
+ popupPositionProvider = popupPositionProvider,
+ properties = properties,
+ ) {
+ DropdownMenuContent(
+ expandedStates = expandedStates,
+ transformOriginState = transformOriginState,
+ modifier = modifier,
+ content = content,
+ )
+ }
+ }
+}
+
+private const val IN_TRANSITION_DURATION = 120
+private const val OUT_TRANSITION_DURATION = 75
+
+@Suppress("ReusedModifierInstance", "MagicNumber")
+@Composable
+private fun DropdownMenuContent(
+ expandedStates: MutableTransitionState,
+ transformOriginState: MutableState,
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ // Menu open/close animation.
+ val transition = rememberTransition(expandedStates, "DropDownMenu")
+
+ val scale by transition.animateFloat(
+ transitionSpec = {
+ if (false isTransitioningTo true) {
+ // Dismissed to expanded
+ tween(
+ durationMillis = IN_TRANSITION_DURATION,
+ easing = LinearOutSlowInEasing,
+ )
+ } else {
+ // Expanded to dismissed.
+ tween(
+ durationMillis = 1,
+ delayMillis = OUT_TRANSITION_DURATION - 1,
+ )
+ }
+ },
+ label = "",
+ ) { isExpanded ->
+ if (isExpanded) {
+ // Menu is expanded.
+ 1f
+ } else {
+ // Menu is dismissed.
+ 0.8f
+ }
+ }
+
+ val alpha by transition.animateFloat(
+ transitionSpec = {
+ if (false isTransitioningTo true) {
+ // Dismissed to expanded
+ tween(durationMillis = 30)
+ } else {
+ // Expanded to dismissed.
+ tween(durationMillis = OUT_TRANSITION_DURATION)
+ }
+ },
+ label = "",
+ ) { isExpanded ->
+ if (isExpanded) {
+ // Menu is expanded.
+ 1f
+ } else {
+ // Menu is dismissed.
+ 0f
+ }
+ }
+ Card(
+ modifier = Modifier
+ .clip(RoundedCornerShape(TangemTheme.dimens2.x5))
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ this.alpha = alpha
+ transformOrigin = transformOriginState.value
+ },
+ elevation = CardDefaults.cardElevation(),
+ ) {
+ Column(
+ modifier = modifier
+ .width(IntrinsicSize.Max)
+ .verticalScroll(rememberScrollState())
+ .clip(RoundedCornerShape(TangemTheme.dimens2.x5))
+ .background(TangemTheme.colors2.contextMenu.background)
+ .testTag(PopUpMenuTestTags.CONTAINER),
+ content = content,
+ )
+ }
+}
+
+private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin {
+ val pivotX = when {
+ menuBounds.left >= parentBounds.right -> 0f
+ menuBounds.right <= parentBounds.left -> 1f
+ menuBounds.width == 0 -> 0f
+ else -> {
+ val intersectionCenter =
+ (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2
+ (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
+ }
+ }
+ val pivotY = when {
+ menuBounds.top >= parentBounds.bottom -> 0f
+ menuBounds.bottom <= parentBounds.top -> 1f
+ menuBounds.height == 0 -> 0f
+ else -> {
+ val intersectionCenter =
+ (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2
+ (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
+ }
+ }
+ return TransformOrigin(pivotX, pivotY)
+}
+
+private val MenuVerticalMargin = 48.dp
+
+@Immutable
+internal data class DropdownMenuPositionProvider(
+ val contentOffset: DpOffset,
+ val density: Density,
+ val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> },
+) : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize,
+ ): IntOffset {
+ // The min margin above and below the menu, relative to the screen.
+ val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
+ // The content offset specified using the dropdown offset parameter.
+ val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
+ val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
+
+ // Compute horizontal position.
+ val toRight = anchorBounds.left + contentOffsetX
+ val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
+ val toDisplayRight = windowSize.width - popupContentSize.width
+ val toDisplayLeft = 0
+ val x = if (layoutDirection == LayoutDirection.Ltr) {
+ sequenceOf(
+ toRight,
+ toLeft,
+ // If the anchor gets outside of the window on the left, we want to position
+ // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
+ if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft,
+ )
+ } else {
+ sequenceOf(
+ toLeft,
+ toRight,
+ // If the anchor gets outside of the window on the right, we want to position
+ // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
+ if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight,
+ )
+ }.firstOrNull {
+ it >= 0 && it + popupContentSize.width <= windowSize.width
+ } ?: toLeft
+
+ // Compute vertical position.
+ val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
+ val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
+ val toCenter = anchorBounds.top - popupContentSize.height / 2
+ val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
+ val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { element ->
+ element >= verticalMargin &&
+ element + popupContentSize.height <= windowSize.height - verticalMargin
+ } ?: toTop
+
+ onPositionCalculated(
+ anchorBounds,
+ IntRect(
+ left = x,
+ top = y,
+ right = x + popupContentSize.width,
+ bottom = y + popupContentSize.height,
+ ),
+ )
+ return IntOffset(x, y)
+ }
+}
+
+// region Preview
+@Composable
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun TangemContextMenu_Preview() {
+ TangemThemePreviewRedesign {
+ val hazeState = rememberHazeState()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(TangemTheme.colors2.surface.level1)
+ .hazeSourceTangem(state = hazeState, zIndex = -1f),
+ ) {
+ TangemContextMenu(
+ expanded = true,
+ onDismissRequest = { },
+ modifier = Modifier.hazeEffectTangem(state = hazeState),
+ ) {
+ TangemContextMenuCheckboxItem(
+ title = stringReference("Sort by balance"),
+ isChecked = true,
+ onClick = {},
+ )
+ HorizontalDivider(
+ thickness = 0.5.dp,
+ color = TangemTheme.colors2.border.neutral.quaternary,
+ )
+ TangemContextMenuCheckboxItem(
+ title = stringReference("Group tokens"),
+ isChecked = false,
+ onClick = {},
+ )
+ }
+ }
+ }
+}
+// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt
new file mode 100644
index 0000000000..8cb5703384
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt
@@ -0,0 +1,43 @@
+package com.tangem.core.ui.ds.contextmenu
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.ds.checkbox.TangemCheckbox
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.clickableSingle
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.res.TangemTheme
+
+/**
+ * Item with checkbox for [TangemContextMenu].
+ */
+@Composable
+fun TangemContextMenuCheckboxItem(title: TextReference, isChecked: Boolean, onClick: () -> Unit) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .width(238.dp)
+ .clickableSingle(onClick = onClick)
+ .padding(
+ vertical = TangemTheme.dimens2.x5,
+ horizontal = TangemTheme.dimens2.x4,
+ ),
+ ) {
+ Text(
+ text = title.resolveReference(),
+ style = TangemTheme.typography2.headingSemibold17,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ )
+ TangemCheckbox(
+ modifier = Modifier,
+ isRounded = true,
+ isChecked = isChecked,
+ onCheckedChange = { onClick() },
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt
new file mode 100644
index 0000000000..6d6d6f046d
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt
@@ -0,0 +1,202 @@
+package com.tangem.core.ui.ds.image
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Icon
+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.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.R
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+
+/**
+ * Composable function for displaying a wallet icon based on the provided [DeviceIconUM] state.
+ *
+ * The icon can represent different types of devices, such as cards, rings, stubs, or mobile wallet,
+ * with customizable colors and styles.
+ *
+ * @param state The state of the device icon, which determines its appearance.
+ * @param modifier Optional [Modifier] for styling the composable.
+ */
+@Composable
+fun TangemDeviceIcon(state: DeviceIconUM, modifier: Modifier = Modifier) {
+ when (state) {
+ is DeviceIconUM.Card -> DeviceIcon(
+ modifier = modifier,
+ isRing = false,
+ mainColor = state.mainColor,
+ secondColor = state.secondColor,
+ thirdColor = state.thirdColor,
+ tColor = null,
+ )
+ is DeviceIconUM.Ring -> DeviceIcon(
+ modifier = modifier,
+ isRing = true,
+ mainColor = state.mainColor,
+ secondColor = state.cardColor,
+ thirdColor = state.secondCardColor,
+ tColor = null,
+ )
+ is DeviceIconUM.Stub -> DeviceIcon(
+ modifier = modifier,
+ isRing = false,
+ mainColor = Color.Unspecified,
+ secondColor = Color.Unspecified.takeIf { state.cardsCount > 1 },
+ thirdColor = Color.Unspecified.takeIf { state.cardsCount > 2 },
+ tColor = TangemTheme.colors2.graphic.neutral.secondary,
+ )
+ DeviceIconUM.Mobile -> Icon(
+ modifier = modifier,
+ imageVector = ImageVector.vectorResource(R.drawable.ic_shield_24),
+ contentDescription = null,
+ tint = TangemTheme.colors2.graphic.status.attention,
+ )
+ }
+}
+
+@Composable
+private fun DeviceIcon(
+ isRing: Boolean,
+ mainColor: Color,
+ secondColor: Color?,
+ thirdColor: Color?,
+ tColor: Color?,
+ modifier: Modifier = Modifier,
+) {
+ val main = mainColor.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
+ val second = secondColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
+ val third = thirdColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
+ val borderColor = TangemTheme.colors2.border.walletIcon
+
+ val imageVector = remember(isRing, main, second, third, borderColor, tColor) {
+ when {
+ isRing && second != null && third != null -> WalletIconVectorBuilders.buildRingWithCard2(
+ mainColor = main,
+ cardColor = second,
+ secondCardColor = third,
+ borderColor = borderColor,
+ )
+ !isRing && second != null && third != null -> WalletIconVectorBuilders.buildCard3(
+ mainColor = main,
+ secondColor = second,
+ thirdColor = third,
+ tColor = tColor,
+ borderColor = borderColor,
+ )
+ isRing && second != null -> WalletIconVectorBuilders.buildRingWithCard(
+ mainColor = main,
+ cardColor = second,
+ borderColor = borderColor,
+ )
+ !isRing && second != null -> WalletIconVectorBuilders.buildCard2(
+ mainColor = main,
+ secondColor = second,
+ tColor = tColor,
+ borderColor = borderColor,
+ )
+ isRing -> WalletIconVectorBuilders.buildRing(
+ mainColor = main,
+ borderColor = borderColor,
+ )
+ else -> WalletIconVectorBuilders.buildCard(
+ mainColor = main,
+ borderColor = borderColor,
+ tColor = tColor,
+ )
+ }
+ }
+
+ Icon(
+ imageVector = imageVector,
+ contentDescription = null,
+ modifier = modifier,
+ tint = Color.Unspecified,
+ )
+}
+
+// region Preview
+
+private val previewCardBlue
+ get() = Color(0xFF1C5FBF)
+private val previewCardGold
+ get() = Color(0xFFD4A017)
+private val previewCardPurple
+ get() = Color(0xFF7B2FBE)
+private val previewRingGreen
+ get() = Color(0xFF2ECC71)
+
+private val previewStates: List>
+ get() = listOf(
+ "Card 1" to DeviceIconUM.Card(
+ mainColor = previewCardBlue,
+ secondColor = null,
+ ),
+ "Card 2" to DeviceIconUM.Card(
+ mainColor = previewCardBlue,
+ secondColor = previewCardGold,
+ ),
+ "Card 3" to DeviceIconUM.Card(
+ mainColor = previewCardBlue,
+ secondColor = previewCardGold,
+ thirdColor = previewCardPurple,
+ ),
+ "Ring" to DeviceIconUM.Ring(
+ mainColor = previewRingGreen,
+ ),
+ "Ring + Card" to DeviceIconUM.Ring(
+ mainColor = previewRingGreen,
+ cardColor = previewCardBlue,
+ ),
+ "Ring + 2 Cards" to DeviceIconUM.Ring(
+ mainColor = previewRingGreen,
+ cardColor = previewCardBlue,
+ secondCardColor = previewCardGold,
+ ),
+ "Stub 1" to DeviceIconUM.Stub(cardsCount = 1),
+ "Stub 2" to DeviceIconUM.Stub(cardsCount = 2),
+ "Stub 3" to DeviceIconUM.Stub(cardsCount = 3),
+ "Mobile" to DeviceIconUM.Mobile,
+ )
+
+@Composable
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun TangemDeviceIcon_Preview() {
+ TangemThemePreviewRedesign {
+ Column(
+ modifier = Modifier
+ .background(TangemTheme.colors.background.primary)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ previewStates.forEach { (label, state) ->
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ TangemDeviceIcon(
+ modifier = Modifier.size(40.dp),
+ state = state,
+ )
+ Text(
+ text = label,
+ style = TangemTheme.typography.body1,
+ color = TangemTheme.colors.text.primary1,
+ )
+ }
+ }
+ }
+ }
+}
+
+// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt
new file mode 100644
index 0000000000..0007a0f1a8
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt
@@ -0,0 +1,24 @@
+package com.tangem.core.ui.ds.image
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+@Immutable
+sealed interface DeviceIconUM {
+
+ data class Card(
+ val mainColor: Color,
+ val secondColor: Color?,
+ val thirdColor: Color? = null,
+ ) : DeviceIconUM
+
+ data class Ring(
+ val mainColor: Color = Color.Unspecified,
+ val cardColor: Color? = null,
+ val secondCardColor: Color? = null,
+ ) : DeviceIconUM
+
+ data class Stub(val cardsCount: Int) : DeviceIconUM
+
+ data object Mobile : DeviceIconUM
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt
index ff7be95be2..2c6ecb3b07 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt
@@ -2,12 +2,19 @@ package com.tangem.core.ui.ds.image
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.identicon.IdentIcon
@@ -40,6 +47,11 @@ sealed interface TangemIconUM {
data class Ident(
val text: String,
) : TangemIconUM
+
+ /** Image represented from network by url */
+ data class Url(
+ val url: String,
+ ) : TangemIconUM
}
/**
@@ -72,5 +84,24 @@ fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) {
address = tangemIconUM.text,
modifier = modifier,
)
+ is TangemIconUM.Url -> SubcomposeAsyncImage(
+ modifier = modifier,
+ model = ImageRequest.Builder(context = LocalContext.current)
+ .data(tangemIconUM.url)
+ .crossfade(enable = true)
+ .allowHardware(enable = false)
+ .build(),
+ loading = { CircleShimmer() },
+ error = {
+ Box(
+ modifier = Modifier
+ .background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = CircleShape,
+ ),
+ )
+ },
+ contentDescription = null,
+ )
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt
new file mode 100644
index 0000000000..993fdf132f
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt
@@ -0,0 +1,1131 @@
+@file:Suppress("MagicNumber", "LargeClass", "LongMethod", "NamedArguments")
+package com.tangem.core.ui.ds.image
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.PathData
+import androidx.compose.ui.graphics.vector.group
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+/**
+ * Builders for creating [ImageVector] instances for wallet icons.
+ * These builders are used to generate the vector graphics for the wallet icons
+ *
+ * Generated based on SVG paths.
+ */
+internal object WalletIconVectorBuilders {
+
+ fun buildRing(mainColor: Color, borderColor: Color): ImageVector = ImageVector.Builder(
+ name = "Ring",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(
+ fill = SolidColor(mainColor),
+ pathFillType = PathFillType.EvenOdd,
+ ) {
+ moveTo(10.333f, 3f)
+ curveTo(7.94f, 3f, 6f, 7.029f, 6f, 12f)
+ curveTo(6f, 16.971f, 7.94f, 21f, 10.333f, 21f)
+ horizontalLineTo(14.667f)
+ curveTo(17.06f, 21f, 19f, 16.971f, 19f, 12f)
+ curveTo(19f, 7.029f, 17.06f, 3f, 14.667f, 3f)
+ horizontalLineTo(10.333f)
+ close()
+ moveTo(10.574f, 3.9f)
+ curveTo(16.403f, 3.9f, 16.498f, 20.1f, 10.574f, 20.1f)
+ curveTo(10.541f, 20.1f, 10.539f, 20.052f, 10.571f, 20.045f)
+ curveTo(11.037f, 19.937f, 11.478f, 19.675f, 11.883f, 19.285f)
+ curveTo(11.961f, 19.21f, 11.96f, 19.087f, 11.889f, 19.005f)
+ curveTo(10.671f, 17.602f, 9.852f, 14.99f, 9.852f, 12f)
+ curveTo(9.852f, 9.009f, 10.671f, 6.397f, 11.89f, 4.994f)
+ curveTo(11.961f, 4.913f, 11.961f, 4.789f, 11.883f, 4.714f)
+ curveTo(11.479f, 4.324f, 11.037f, 4.062f, 10.571f, 3.955f)
+ curveTo(10.539f, 3.948f, 10.541f, 3.9f, 10.574f, 3.9f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(14.667f, 3.5f)
+ horizontalLineTo(11.398f)
+ curveTo(12.705f, 3.818f, 13.671f, 4.88f, 14.316f, 6.213f)
+ curveTo(15.094f, 7.818f, 15.475f, 9.923f, 15.481f, 11.998f)
+ curveTo(15.488f, 14.073f, 15.118f, 16.179f, 14.343f, 17.786f)
+ curveTo(13.698f, 19.122f, 12.728f, 20.183f, 11.407f, 20.5f)
+ horizontalLineTo(14.667f)
+ curveTo(15.562f, 20.5f, 16.518f, 19.73f, 17.28f, 18.147f)
+ curveTo(18.025f, 16.6f, 18.5f, 14.427f, 18.5f, 12f)
+ curveTo(18.5f, 9.573f, 18.025f, 7.4f, 17.28f, 5.853f)
+ curveTo(16.518f, 4.27f, 15.562f, 3.5f, 14.667f, 3.5f)
+ close()
+ moveTo(10.264f, 3.503f)
+ curveTo(9.389f, 3.542f, 8.462f, 4.311f, 7.72f, 5.853f)
+ curveTo(6.975f, 7.4f, 6.5f, 9.573f, 6.5f, 12f)
+ curveTo(6.5f, 14.427f, 6.975f, 16.6f, 7.72f, 18.147f)
+ curveTo(8.462f, 19.688f, 9.388f, 20.457f, 10.263f, 20.496f)
+ curveTo(10.238f, 20.478f, 10.212f, 20.458f, 10.19f, 20.435f)
+ curveTo(10.094f, 20.332f, 10.054f, 20.208f, 10.049f, 20.101f)
+ curveTo(10.038f, 19.888f, 10.168f, 19.625f, 10.459f, 19.558f)
+ curveTo(10.748f, 19.491f, 11.04f, 19.341f, 11.329f, 19.106f)
+ curveTo(10.111f, 17.543f, 9.352f, 14.913f, 9.352f, 12f)
+ curveTo(9.352f, 9.086f, 10.111f, 6.455f, 11.33f, 4.892f)
+ curveTo(11.041f, 4.657f, 10.748f, 4.509f, 10.459f, 4.442f)
+ curveTo(10.171f, 4.376f, 10.038f, 4.115f, 10.049f, 3.898f)
+ curveTo(10.055f, 3.79f, 10.096f, 3.666f, 10.192f, 3.563f)
+ curveTo(10.214f, 3.54f, 10.239f, 3.521f, 10.264f, 3.503f)
+ close()
+ }
+ }.build()
+
+ fun buildRingWithCard(mainColor: Color, cardColor: Color, borderColor: Color): ImageVector = ImageVector.Builder(
+ name = "RingWithCard",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(
+ fill = SolidColor(mainColor),
+ pathFillType = PathFillType.EvenOdd,
+ ) {
+ moveTo(4.667f, 10f)
+ curveTo(3.194f, 10f, 2f, 12.462f, 2f, 15.5f)
+ curveTo(2f, 18.538f, 3.194f, 21f, 4.667f, 21f)
+ horizontalLineTo(7.333f)
+ curveTo(8.806f, 21f, 10f, 18.538f, 10f, 15.5f)
+ curveTo(10f, 12.462f, 8.806f, 10f, 7.333f, 10f)
+ horizontalLineTo(4.667f)
+ close()
+ moveTo(4.815f, 10.55f)
+ curveTo(8.402f, 10.55f, 8.46f, 20.45f, 4.815f, 20.45f)
+ curveTo(4.795f, 20.45f, 4.793f, 20.421f, 4.813f, 20.416f)
+ curveTo(5.099f, 20.351f, 5.371f, 20.19f, 5.62f, 19.952f)
+ curveTo(5.668f, 19.906f, 5.668f, 19.83f, 5.624f, 19.781f)
+ curveTo(4.874f, 18.923f, 4.37f, 17.327f, 4.37f, 15.5f)
+ curveTo(4.37f, 13.672f, 4.875f, 12.076f, 5.624f, 11.219f)
+ curveTo(5.668f, 11.169f, 5.668f, 11.093f, 5.62f, 11.047f)
+ curveTo(5.371f, 10.809f, 5.1f, 10.649f, 4.813f, 10.584f)
+ curveTo(4.793f, 10.579f, 4.795f, 10.55f, 4.815f, 10.55f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(7.333f, 10.5f)
+ horizontalLineTo(6.192f)
+ curveTo(6.657f, 10.835f, 7.021f, 11.328f, 7.29f, 11.878f)
+ curveTo(7.785f, 12.893f, 8.023f, 14.21f, 8.027f, 15.498f)
+ curveTo(8.031f, 16.786f, 7.801f, 18.106f, 7.307f, 19.122f)
+ curveTo(7.038f, 19.674f, 6.674f, 20.166f, 6.207f, 20.5f)
+ horizontalLineTo(7.333f)
+ curveTo(7.77f, 20.5f, 8.31f, 20.119f, 8.77f, 19.171f)
+ curveTo(9.212f, 18.257f, 9.5f, 16.96f, 9.5f, 15.5f)
+ curveTo(9.5f, 14.04f, 9.212f, 12.743f, 8.77f, 11.829f)
+ curveTo(8.31f, 10.881f, 7.77f, 10.5f, 7.333f, 10.5f)
+ close()
+ moveTo(4.302f, 10.584f)
+ curveTo(3.95f, 10.742f, 3.568f, 11.132f, 3.23f, 11.829f)
+ curveTo(2.788f, 12.743f, 2.5f, 14.04f, 2.5f, 15.5f)
+ curveTo(2.5f, 16.96f, 2.788f, 18.257f, 3.23f, 19.171f)
+ curveTo(3.568f, 19.868f, 3.95f, 20.257f, 4.302f, 20.415f)
+ curveTo(4.31f, 20.216f, 4.436f, 19.99f, 4.701f, 19.929f)
+ curveTo(4.8f, 19.906f, 4.902f, 19.863f, 5.008f, 19.798f)
+ curveTo(4.295f, 18.785f, 3.87f, 17.209f, 3.87f, 15.5f)
+ curveTo(3.87f, 13.791f, 4.294f, 12.215f, 5.007f, 11.201f)
+ curveTo(4.901f, 11.137f, 4.799f, 11.094f, 4.701f, 11.071f)
+ curveTo(4.438f, 11.011f, 4.31f, 10.785f, 4.302f, 10.584f)
+ close()
+ }
+ path(fill = SolidColor(cardColor)) {
+ moveTo(16.2f, 4.998f)
+ curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f)
+ curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f)
+ curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f)
+ verticalLineTo(14.197f)
+ curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f)
+ curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f)
+ curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f)
+ horizontalLineTo(10.509f)
+ curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f)
+ curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f)
+ curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f)
+ horizontalLineTo(4.6f)
+ curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f)
+ curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f)
+ curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f)
+ curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f)
+ horizontalLineTo(16.2f)
+ close()
+ }
+ group(
+ clipPathData = PathData {
+ moveTo(16.2f, 4.998f)
+ curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f)
+ curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f)
+ curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f)
+ verticalLineTo(14.197f)
+ curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f)
+ curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f)
+ curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f)
+ horizontalLineTo(10.509f)
+ curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f)
+ curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f)
+ curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f)
+ horizontalLineTo(4.6f)
+ curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f)
+ curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f)
+ curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f)
+ curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f)
+ horizontalLineTo(16.2f)
+ close()
+ },
+ ) {
+ path(
+ fill = SolidColor(borderColor),
+ ) {
+ moveTo(16.2f, 4.998f)
+ verticalLineTo(3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(19.362f, 5.324f)
+ lineTo(19.816f, 4.433f)
+ lineTo(19.816f, 4.433f)
+ lineTo(19.362f, 5.324f)
+ close()
+ moveTo(20.673f, 6.636f)
+ lineTo(21.564f, 6.182f)
+ lineTo(21.564f, 6.182f)
+ lineTo(20.673f, 6.636f)
+ close()
+ moveTo(20.673f, 17.359f)
+ lineTo(21.564f, 17.813f)
+ lineTo(21.564f, 17.813f)
+ lineTo(20.673f, 17.359f)
+ close()
+ moveTo(19.362f, 18.671f)
+ lineTo(19.816f, 19.562f)
+ lineTo(19.816f, 19.562f)
+ lineTo(19.362f, 18.671f)
+ close()
+ moveTo(16.2f, 18.998f)
+ verticalLineTo(19.998f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(10.509f, 18.998f)
+ lineTo(9.561f, 18.679f)
+ lineTo(9.116f, 19.998f)
+ horizontalLineTo(10.509f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(10.132f, 11.081f)
+ lineTo(11.028f, 10.638f)
+ lineTo(11.028f, 10.638f)
+ lineTo(10.132f, 11.081f)
+ close()
+ moveTo(7.488f, 9f)
+ lineTo(7.488f, 8f)
+ horizontalLineTo(7.488f)
+ verticalLineTo(9f)
+ close()
+ moveTo(4.6f, 9f)
+ verticalLineTo(8f)
+ horizontalLineTo(4.6f)
+ lineTo(4.6f, 9f)
+ close()
+ moveTo(3f, 9.652f)
+ lineTo(2f, 9.652f)
+ lineTo(2f, 11.875f)
+ lineTo(3.663f, 10.401f)
+ lineTo(3f, 9.652f)
+ close()
+ moveTo(3.327f, 6.636f)
+ lineTo(2.436f, 6.182f)
+ lineTo(2.436f, 6.182f)
+ lineTo(3.327f, 6.636f)
+ close()
+ moveTo(4.638f, 5.324f)
+ lineTo(4.184f, 4.433f)
+ lineTo(4.184f, 4.433f)
+ lineTo(4.638f, 5.324f)
+ close()
+ moveTo(7.8f, 4.998f)
+ verticalLineTo(3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(16.2f, 4.998f)
+ verticalLineTo(5.998f)
+ curveTo(17.057f, 5.998f, 17.639f, 5.999f, 18.09f, 6.035f)
+ curveTo(18.528f, 6.071f, 18.752f, 6.136f, 18.908f, 6.215f)
+ lineTo(19.362f, 5.324f)
+ lineTo(19.816f, 4.433f)
+ curveTo(19.331f, 4.186f, 18.814f, 4.087f, 18.252f, 4.042f)
+ curveTo(17.701f, 3.997f, 17.024f, 3.998f, 16.2f, 3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(19.362f, 5.324f)
+ lineTo(18.908f, 6.215f)
+ curveTo(19.284f, 6.407f, 19.59f, 6.713f, 19.782f, 7.09f)
+ lineTo(20.673f, 6.636f)
+ lineTo(21.564f, 6.182f)
+ curveTo(21.181f, 5.43f, 20.569f, 4.817f, 19.816f, 4.433f)
+ lineTo(19.362f, 5.324f)
+ close()
+ moveTo(20.673f, 6.636f)
+ lineTo(19.782f, 7.09f)
+ curveTo(19.861f, 7.246f, 19.927f, 7.47f, 19.962f, 7.909f)
+ curveTo(19.999f, 8.359f, 20f, 8.941f, 20f, 9.798f)
+ horizontalLineTo(21f)
+ horizontalLineTo(22f)
+ curveTo(22f, 8.974f, 22.001f, 8.296f, 21.956f, 7.746f)
+ curveTo(21.91f, 7.184f, 21.811f, 6.667f, 21.564f, 6.182f)
+ lineTo(20.673f, 6.636f)
+ close()
+ moveTo(21f, 9.798f)
+ horizontalLineTo(20f)
+ verticalLineTo(14.197f)
+ horizontalLineTo(21f)
+ horizontalLineTo(22f)
+ verticalLineTo(9.798f)
+ horizontalLineTo(21f)
+ close()
+ moveTo(21f, 14.197f)
+ horizontalLineTo(20f)
+ curveTo(20f, 15.054f, 19.999f, 15.636f, 19.962f, 16.086f)
+ curveTo(19.927f, 16.525f, 19.861f, 16.749f, 19.782f, 16.905f)
+ lineTo(20.673f, 17.359f)
+ lineTo(21.564f, 17.813f)
+ curveTo(21.811f, 17.328f, 21.91f, 16.811f, 21.956f, 16.249f)
+ curveTo(22.001f, 15.699f, 22f, 15.021f, 22f, 14.197f)
+ horizontalLineTo(21f)
+ close()
+ moveTo(20.673f, 17.359f)
+ lineTo(19.782f, 16.905f)
+ curveTo(19.59f, 17.282f, 19.284f, 17.588f, 18.908f, 17.78f)
+ lineTo(19.362f, 18.671f)
+ lineTo(19.816f, 19.562f)
+ curveTo(20.569f, 19.178f, 21.181f, 18.565f, 21.564f, 17.813f)
+ lineTo(20.673f, 17.359f)
+ close()
+ moveTo(19.362f, 18.671f)
+ lineTo(18.908f, 17.78f)
+ curveTo(18.752f, 17.86f, 18.528f, 17.925f, 18.089f, 17.96f)
+ curveTo(17.639f, 17.997f, 17.057f, 17.998f, 16.2f, 17.998f)
+ verticalLineTo(18.998f)
+ verticalLineTo(19.998f)
+ curveTo(17.024f, 19.998f, 17.702f, 19.999f, 18.252f, 19.954f)
+ curveTo(18.814f, 19.908f, 19.331f, 19.809f, 19.816f, 19.562f)
+ lineTo(19.362f, 18.671f)
+ close()
+ moveTo(16.2f, 18.998f)
+ verticalLineTo(17.998f)
+ horizontalLineTo(10.509f)
+ verticalLineTo(18.998f)
+ verticalLineTo(19.998f)
+ horizontalLineTo(16.2f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(10.509f, 18.998f)
+ lineTo(11.456f, 19.317f)
+ curveTo(11.837f, 18.188f, 12.044f, 16.874f, 12.044f, 15.5f)
+ horizontalLineTo(11.044f)
+ horizontalLineTo(10.044f)
+ curveTo(10.044f, 16.682f, 9.865f, 17.778f, 9.561f, 18.679f)
+ lineTo(10.509f, 18.998f)
+ close()
+ moveTo(11.044f, 15.5f)
+ horizontalLineTo(12.044f)
+ curveTo(12.044f, 13.689f, 11.685f, 11.968f, 11.028f, 10.638f)
+ lineTo(10.132f, 11.081f)
+ lineTo(9.235f, 11.524f)
+ curveTo(9.729f, 12.523f, 10.044f, 13.929f, 10.044f, 15.5f)
+ horizontalLineTo(11.044f)
+ close()
+ moveTo(10.132f, 11.081f)
+ lineTo(11.028f, 10.638f)
+ curveTo(10.428f, 9.423f, 9.28f, 8f, 7.488f, 8f)
+ lineTo(7.488f, 9f)
+ lineTo(7.488f, 10f)
+ curveTo(8.095f, 10f, 8.731f, 10.503f, 9.235f, 11.524f)
+ lineTo(10.132f, 11.081f)
+ close()
+ moveTo(7.488f, 9f)
+ verticalLineTo(8f)
+ horizontalLineTo(4.6f)
+ verticalLineTo(9f)
+ verticalLineTo(10f)
+ horizontalLineTo(7.488f)
+ verticalLineTo(9f)
+ close()
+ moveTo(4.6f, 9f)
+ lineTo(4.6f, 8f)
+ curveTo(3.687f, 8f, 2.925f, 8.382f, 2.337f, 8.904f)
+ lineTo(3f, 9.652f)
+ lineTo(3.663f, 10.401f)
+ curveTo(3.973f, 10.126f, 4.281f, 10f, 4.6f, 10f)
+ lineTo(4.6f, 9f)
+ close()
+ moveTo(3f, 9.652f)
+ lineTo(4f, 9.653f)
+ curveTo(4f, 8.847f, 4.003f, 8.297f, 4.041f, 7.871f)
+ curveTo(4.077f, 7.457f, 4.141f, 7.241f, 4.218f, 7.09f)
+ lineTo(3.327f, 6.636f)
+ lineTo(2.436f, 6.182f)
+ curveTo(2.196f, 6.653f, 2.096f, 7.153f, 2.049f, 7.696f)
+ curveTo(2.002f, 8.227f, 2f, 8.874f, 2f, 9.652f)
+ lineTo(3f, 9.652f)
+ close()
+ moveTo(3.327f, 6.636f)
+ lineTo(4.218f, 7.09f)
+ curveTo(4.41f, 6.713f, 4.716f, 6.407f, 5.092f, 6.215f)
+ lineTo(4.638f, 5.324f)
+ lineTo(4.184f, 4.433f)
+ curveTo(3.43f, 4.817f, 2.819f, 5.43f, 2.436f, 6.182f)
+ lineTo(3.327f, 6.636f)
+ close()
+ moveTo(4.638f, 5.324f)
+ lineTo(5.092f, 6.215f)
+ curveTo(5.248f, 6.136f, 5.472f, 6.071f, 5.91f, 6.035f)
+ curveTo(6.361f, 5.999f, 6.943f, 5.998f, 7.8f, 5.998f)
+ verticalLineTo(4.998f)
+ verticalLineTo(3.998f)
+ curveTo(6.977f, 3.998f, 6.299f, 3.997f, 5.748f, 4.042f)
+ curveTo(5.186f, 4.087f, 4.669f, 4.186f, 4.184f, 4.433f)
+ lineTo(4.638f, 5.324f)
+ close()
+ moveTo(7.8f, 4.998f)
+ verticalLineTo(5.998f)
+ horizontalLineTo(16.2f)
+ verticalLineTo(4.998f)
+ verticalLineTo(3.998f)
+ horizontalLineTo(7.8f)
+ verticalLineTo(4.998f)
+ close()
+ }
+ }
+ }.build()
+
+ fun buildRingWithCard2(
+ mainColor: Color,
+ cardColor: Color,
+ secondCardColor: Color,
+ borderColor: Color,
+ ): ImageVector = ImageVector.Builder(
+ name = "RingTwoCards",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(
+ fill = SolidColor(mainColor),
+ pathFillType = PathFillType.EvenOdd,
+ ) {
+ moveTo(3.667f, 10f)
+ curveTo(2.194f, 10f, 1f, 12.462f, 1f, 15.5f)
+ curveTo(1f, 18.538f, 2.194f, 21f, 3.667f, 21f)
+ horizontalLineTo(6.333f)
+ curveTo(7.806f, 21f, 9f, 18.538f, 9f, 15.5f)
+ curveTo(9f, 12.462f, 7.806f, 10f, 6.333f, 10f)
+ horizontalLineTo(3.667f)
+ close()
+ moveTo(3.815f, 10.55f)
+ curveTo(7.402f, 10.55f, 7.46f, 20.45f, 3.815f, 20.45f)
+ curveTo(3.795f, 20.45f, 3.793f, 20.421f, 3.813f, 20.416f)
+ curveTo(4.099f, 20.351f, 4.371f, 20.19f, 4.62f, 19.952f)
+ curveTo(4.668f, 19.906f, 4.668f, 19.83f, 4.624f, 19.781f)
+ curveTo(3.874f, 18.923f, 3.37f, 17.327f, 3.37f, 15.5f)
+ curveTo(3.37f, 13.672f, 3.875f, 12.076f, 4.624f, 11.219f)
+ curveTo(4.668f, 11.169f, 4.668f, 11.093f, 4.62f, 11.047f)
+ curveTo(4.371f, 10.809f, 4.1f, 10.649f, 3.813f, 10.584f)
+ curveTo(3.793f, 10.579f, 3.795f, 10.55f, 3.815f, 10.55f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(6.333f, 10.5f)
+ horizontalLineTo(5.192f)
+ curveTo(5.657f, 10.835f, 6.021f, 11.328f, 6.29f, 11.878f)
+ curveTo(6.785f, 12.893f, 7.023f, 14.21f, 7.027f, 15.498f)
+ curveTo(7.031f, 16.786f, 6.801f, 18.106f, 6.307f, 19.122f)
+ curveTo(6.038f, 19.674f, 5.674f, 20.166f, 5.207f, 20.5f)
+ horizontalLineTo(6.333f)
+ curveTo(6.77f, 20.5f, 7.31f, 20.119f, 7.77f, 19.171f)
+ curveTo(8.212f, 18.257f, 8.5f, 16.96f, 8.5f, 15.5f)
+ curveTo(8.5f, 14.04f, 8.212f, 12.743f, 7.77f, 11.829f)
+ curveTo(7.31f, 10.881f, 6.77f, 10.5f, 6.333f, 10.5f)
+ close()
+ moveTo(3.302f, 10.584f)
+ curveTo(2.95f, 10.742f, 2.568f, 11.132f, 2.23f, 11.829f)
+ curveTo(1.788f, 12.743f, 1.5f, 14.04f, 1.5f, 15.5f)
+ curveTo(1.5f, 16.96f, 1.788f, 18.257f, 2.23f, 19.171f)
+ curveTo(2.568f, 19.868f, 2.95f, 20.257f, 3.302f, 20.415f)
+ curveTo(3.31f, 20.216f, 3.436f, 19.99f, 3.701f, 19.929f)
+ curveTo(3.8f, 19.906f, 3.902f, 19.863f, 4.008f, 19.798f)
+ curveTo(3.295f, 18.785f, 2.87f, 17.209f, 2.87f, 15.5f)
+ curveTo(2.87f, 13.791f, 3.294f, 12.215f, 4.007f, 11.201f)
+ curveTo(3.901f, 11.137f, 3.799f, 11.094f, 3.701f, 11.071f)
+ curveTo(3.438f, 11.011f, 3.31f, 10.785f, 3.302f, 10.584f)
+ close()
+ }
+ path(fill = SolidColor(cardColor)) {
+ moveTo(15.2f, 4.998f)
+ curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f)
+ curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f)
+ curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f)
+ verticalLineTo(14.197f)
+ curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f)
+ curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f)
+ curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f)
+ horizontalLineTo(9.509f)
+ curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f)
+ curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f)
+ curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f)
+ horizontalLineTo(3.6f)
+ curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f)
+ curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f)
+ curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f)
+ curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f)
+ horizontalLineTo(15.2f)
+ close()
+ }
+ group(
+ clipPathData = PathData {
+ moveTo(15.2f, 4.998f)
+ curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f)
+ curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f)
+ curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f)
+ verticalLineTo(14.197f)
+ curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f)
+ curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f)
+ curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f)
+ horizontalLineTo(9.509f)
+ curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f)
+ curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f)
+ curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f)
+ horizontalLineTo(3.6f)
+ curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f)
+ curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f)
+ curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f)
+ curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f)
+ horizontalLineTo(15.2f)
+ close()
+ },
+ ) {
+ path(
+ fill = SolidColor(secondCardColor),
+ fillAlpha = 0.1f,
+ ) {
+ moveTo(15.2f, 4.998f)
+ verticalLineTo(3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(18.362f, 5.324f)
+ lineTo(18.816f, 4.433f)
+ lineTo(18.816f, 4.433f)
+ lineTo(18.362f, 5.324f)
+ close()
+ moveTo(19.673f, 6.636f)
+ lineTo(20.564f, 6.182f)
+ lineTo(20.564f, 6.182f)
+ lineTo(19.673f, 6.636f)
+ close()
+ moveTo(19.673f, 17.359f)
+ lineTo(20.564f, 17.813f)
+ lineTo(20.564f, 17.813f)
+ lineTo(19.673f, 17.359f)
+ close()
+ moveTo(18.362f, 18.671f)
+ lineTo(18.816f, 19.562f)
+ lineTo(18.816f, 19.562f)
+ lineTo(18.362f, 18.671f)
+ close()
+ moveTo(15.2f, 18.998f)
+ verticalLineTo(19.998f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(9.509f, 18.998f)
+ lineTo(8.561f, 18.679f)
+ lineTo(8.116f, 19.998f)
+ horizontalLineTo(9.509f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(9.132f, 11.081f)
+ lineTo(10.028f, 10.638f)
+ lineTo(10.028f, 10.638f)
+ lineTo(9.132f, 11.081f)
+ close()
+ moveTo(6.488f, 9f)
+ lineTo(6.488f, 8f)
+ horizontalLineTo(6.488f)
+ verticalLineTo(9f)
+ close()
+ moveTo(3.6f, 9f)
+ verticalLineTo(8f)
+ horizontalLineTo(3.6f)
+ lineTo(3.6f, 9f)
+ close()
+ moveTo(2f, 9.652f)
+ lineTo(1f, 9.652f)
+ lineTo(1f, 11.875f)
+ lineTo(2.663f, 10.401f)
+ lineTo(2f, 9.652f)
+ close()
+ moveTo(2.327f, 6.636f)
+ lineTo(1.436f, 6.182f)
+ lineTo(1.436f, 6.182f)
+ lineTo(2.327f, 6.636f)
+ close()
+ moveTo(3.638f, 5.324f)
+ lineTo(3.184f, 4.433f)
+ lineTo(3.184f, 4.433f)
+ lineTo(3.638f, 5.324f)
+ close()
+ moveTo(6.8f, 4.998f)
+ verticalLineTo(3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(15.2f, 4.998f)
+ verticalLineTo(5.998f)
+ curveTo(16.057f, 5.998f, 16.639f, 5.999f, 17.09f, 6.035f)
+ curveTo(17.528f, 6.071f, 17.752f, 6.136f, 17.908f, 6.215f)
+ lineTo(18.362f, 5.324f)
+ lineTo(18.816f, 4.433f)
+ curveTo(18.331f, 4.186f, 17.814f, 4.087f, 17.252f, 4.042f)
+ curveTo(16.701f, 3.997f, 16.024f, 3.998f, 15.2f, 3.998f)
+ verticalLineTo(4.998f)
+ close()
+ moveTo(18.362f, 5.324f)
+ lineTo(17.908f, 6.215f)
+ curveTo(18.284f, 6.407f, 18.59f, 6.713f, 18.782f, 7.09f)
+ lineTo(19.673f, 6.636f)
+ lineTo(20.564f, 6.182f)
+ curveTo(20.181f, 5.43f, 19.569f, 4.817f, 18.816f, 4.433f)
+ lineTo(18.362f, 5.324f)
+ close()
+ moveTo(19.673f, 6.636f)
+ lineTo(18.782f, 7.09f)
+ curveTo(18.861f, 7.246f, 18.927f, 7.47f, 18.962f, 7.909f)
+ curveTo(18.999f, 8.359f, 19f, 8.941f, 19f, 9.798f)
+ horizontalLineTo(20f)
+ horizontalLineTo(21f)
+ curveTo(21f, 8.974f, 21.001f, 8.296f, 20.956f, 7.746f)
+ curveTo(20.91f, 7.184f, 20.811f, 6.667f, 20.564f, 6.182f)
+ lineTo(19.673f, 6.636f)
+ close()
+ moveTo(20f, 9.798f)
+ horizontalLineTo(19f)
+ verticalLineTo(14.197f)
+ horizontalLineTo(20f)
+ horizontalLineTo(21f)
+ verticalLineTo(9.798f)
+ horizontalLineTo(20f)
+ close()
+ moveTo(20f, 14.197f)
+ horizontalLineTo(19f)
+ curveTo(19f, 15.054f, 18.999f, 15.636f, 18.962f, 16.086f)
+ curveTo(18.927f, 16.525f, 18.861f, 16.749f, 18.782f, 16.905f)
+ lineTo(19.673f, 17.359f)
+ lineTo(20.564f, 17.813f)
+ curveTo(20.811f, 17.328f, 20.91f, 16.811f, 20.956f, 16.249f)
+ curveTo(21.001f, 15.699f, 21f, 15.021f, 21f, 14.197f)
+ horizontalLineTo(20f)
+ close()
+ moveTo(19.673f, 17.359f)
+ lineTo(18.782f, 16.905f)
+ curveTo(18.59f, 17.282f, 18.284f, 17.588f, 17.908f, 17.78f)
+ lineTo(18.362f, 18.671f)
+ lineTo(18.816f, 19.562f)
+ curveTo(19.569f, 19.178f, 20.181f, 18.565f, 20.564f, 17.813f)
+ lineTo(19.673f, 17.359f)
+ close()
+ moveTo(18.362f, 18.671f)
+ lineTo(17.908f, 17.78f)
+ curveTo(17.752f, 17.86f, 17.528f, 17.925f, 17.089f, 17.96f)
+ curveTo(16.639f, 17.997f, 16.057f, 17.998f, 15.2f, 17.998f)
+ verticalLineTo(18.998f)
+ verticalLineTo(19.998f)
+ curveTo(16.024f, 19.998f, 16.702f, 19.999f, 17.252f, 19.954f)
+ curveTo(17.814f, 19.908f, 18.331f, 19.809f, 18.816f, 19.562f)
+ lineTo(18.362f, 18.671f)
+ close()
+ moveTo(15.2f, 18.998f)
+ verticalLineTo(17.998f)
+ horizontalLineTo(9.509f)
+ verticalLineTo(18.998f)
+ verticalLineTo(19.998f)
+ horizontalLineTo(15.2f)
+ verticalLineTo(18.998f)
+ close()
+ moveTo(9.509f, 18.998f)
+ lineTo(10.456f, 19.317f)
+ curveTo(10.837f, 18.188f, 11.044f, 16.874f, 11.044f, 15.5f)
+ horizontalLineTo(10.044f)
+ horizontalLineTo(9.044f)
+ curveTo(9.044f, 16.682f, 8.865f, 17.778f, 8.561f, 18.679f)
+ lineTo(9.509f, 18.998f)
+ close()
+ moveTo(10.044f, 15.5f)
+ horizontalLineTo(11.044f)
+ curveTo(11.044f, 13.689f, 10.685f, 11.968f, 10.028f, 10.638f)
+ lineTo(9.132f, 11.081f)
+ lineTo(8.235f, 11.524f)
+ curveTo(8.729f, 12.523f, 9.044f, 13.929f, 9.044f, 15.5f)
+ horizontalLineTo(10.044f)
+ close()
+ moveTo(9.132f, 11.081f)
+ lineTo(10.028f, 10.638f)
+ curveTo(9.428f, 9.423f, 8.28f, 8f, 6.488f, 8f)
+ lineTo(6.488f, 9f)
+ lineTo(6.488f, 10f)
+ curveTo(7.095f, 10f, 7.731f, 10.503f, 8.235f, 11.524f)
+ lineTo(9.132f, 11.081f)
+ close()
+ moveTo(6.488f, 9f)
+ verticalLineTo(8f)
+ horizontalLineTo(3.6f)
+ verticalLineTo(9f)
+ verticalLineTo(10f)
+ horizontalLineTo(6.488f)
+ verticalLineTo(9f)
+ close()
+ moveTo(3.6f, 9f)
+ lineTo(3.6f, 8f)
+ curveTo(2.687f, 8f, 1.925f, 8.382f, 1.337f, 8.904f)
+ lineTo(2f, 9.652f)
+ lineTo(2.663f, 10.401f)
+ curveTo(2.973f, 10.126f, 3.281f, 10f, 3.6f, 10f)
+ lineTo(3.6f, 9f)
+ close()
+ moveTo(2f, 9.652f)
+ lineTo(3f, 9.653f)
+ curveTo(3f, 8.847f, 3.003f, 8.297f, 3.041f, 7.871f)
+ curveTo(3.077f, 7.457f, 3.141f, 7.241f, 3.218f, 7.09f)
+ lineTo(2.327f, 6.636f)
+ lineTo(1.436f, 6.182f)
+ curveTo(1.196f, 6.653f, 1.096f, 7.153f, 1.048f, 7.696f)
+ curveTo(1.002f, 8.227f, 1f, 8.874f, 1f, 9.652f)
+ lineTo(2f, 9.652f)
+ close()
+ moveTo(2.327f, 6.636f)
+ lineTo(3.218f, 7.09f)
+ curveTo(3.41f, 6.713f, 3.716f, 6.407f, 4.092f, 6.215f)
+ lineTo(3.638f, 5.324f)
+ lineTo(3.184f, 4.433f)
+ curveTo(2.43f, 4.817f, 1.819f, 5.43f, 1.436f, 6.182f)
+ lineTo(2.327f, 6.636f)
+ close()
+ moveTo(3.638f, 5.324f)
+ lineTo(4.092f, 6.215f)
+ curveTo(4.248f, 6.136f, 4.472f, 6.071f, 4.91f, 6.035f)
+ curveTo(5.361f, 5.999f, 5.943f, 5.998f, 6.8f, 5.998f)
+ verticalLineTo(4.998f)
+ verticalLineTo(3.998f)
+ curveTo(5.977f, 3.998f, 5.299f, 3.997f, 4.748f, 4.042f)
+ curveTo(4.186f, 4.087f, 3.669f, 4.186f, 3.184f, 4.433f)
+ lineTo(3.638f, 5.324f)
+ close()
+ moveTo(6.8f, 4.998f)
+ verticalLineTo(5.998f)
+ horizontalLineTo(15.2f)
+ verticalLineTo(4.998f)
+ verticalLineTo(3.998f)
+ horizontalLineTo(6.8f)
+ verticalLineTo(4.998f)
+ close()
+ }
+ }
+ path(fill = SolidColor(secondCardColor)) {
+ moveTo(18.035f, 5f)
+ curveTo(19.772f, 5f, 20.642f, 5f, 21.306f, 5.327f)
+ curveTo(21.889f, 5.615f, 22.364f, 6.073f, 22.662f, 6.638f)
+ curveTo(23f, 7.279f, 23f, 8.12f, 23f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(23f, 15.88f, 23f, 16.721f, 22.662f, 17.362f)
+ curveTo(22.364f, 17.927f, 21.889f, 18.385f, 21.306f, 18.673f)
+ curveTo(20.642f, 19f, 19.772f, 19f, 18.035f, 19f)
+ horizontalLineTo(17f)
+ curveTo(18.738f, 19f, 19.607f, 19f, 20.271f, 18.673f)
+ curveTo(20.855f, 18.385f, 21.33f, 17.927f, 21.627f, 17.362f)
+ curveTo(21.965f, 16.721f, 21.965f, 15.88f, 21.965f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(21.965f, 8.12f, 21.965f, 7.279f, 21.627f, 6.638f)
+ curveTo(21.33f, 6.073f, 20.855f, 5.615f, 20.271f, 5.327f)
+ curveTo(19.607f, 5f, 18.738f, 5f, 17f, 5f)
+ horizontalLineTo(18.035f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(22.331f, 7.153f)
+ curveTo(22.39f, 7.342f, 22.434f, 7.57f, 22.459f, 7.871f)
+ curveTo(22.499f, 8.345f, 22.5f, 8.951f, 22.5f, 9.8f)
+ verticalLineTo(14.2f)
+ lineTo(22.495f, 15.305f)
+ curveTo(22.49f, 15.621f, 22.479f, 15.892f, 22.459f, 16.129f)
+ curveTo(22.434f, 16.429f, 22.39f, 16.657f, 22.331f, 16.846f)
+ curveTo(22.373f, 16.648f, 22.402f, 16.438f, 22.421f, 16.213f)
+ curveTo(22.465f, 15.687f, 22.466f, 15.032f, 22.466f, 14.2f)
+ verticalLineTo(9.8f)
+ lineTo(22.46f, 8.679f)
+ curveTo(22.455f, 8.345f, 22.443f, 8.05f, 22.421f, 7.787f)
+ curveTo(22.402f, 7.561f, 22.373f, 7.351f, 22.331f, 7.153f)
+ close()
+ }
+ }.build()
+
+ fun buildCard(mainColor: Color, borderColor: Color, tColor: Color?): ImageVector = ImageVector.Builder(
+ name = "KeyCard",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(fill = SolidColor(mainColor)) {
+ moveTo(3f, 9.8f)
+ curveTo(3f, 8.12f, 3f, 7.28f, 3.327f, 6.638f)
+ curveTo(3.615f, 6.074f, 4.074f, 5.615f, 4.638f, 5.327f)
+ curveTo(5.28f, 5f, 6.12f, 5f, 7.8f, 5f)
+ horizontalLineTo(16.2f)
+ curveTo(17.88f, 5f, 18.72f, 5f, 19.362f, 5.327f)
+ curveTo(19.927f, 5.615f, 20.385f, 6.074f, 20.673f, 6.638f)
+ curveTo(21f, 7.28f, 21f, 8.12f, 21f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(21f, 15.88f, 21f, 16.72f, 20.673f, 17.362f)
+ curveTo(20.385f, 17.927f, 19.927f, 18.385f, 19.362f, 18.673f)
+ curveTo(18.72f, 19f, 17.88f, 19f, 16.2f, 19f)
+ horizontalLineTo(7.8f)
+ curveTo(6.12f, 19f, 5.28f, 19f, 4.638f, 18.673f)
+ curveTo(4.074f, 18.385f, 3.615f, 17.927f, 3.327f, 17.362f)
+ curveTo(3f, 16.72f, 3f, 15.88f, 3f, 14.2f)
+ verticalLineTo(9.8f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(7.8f, 5.5f)
+ horizontalLineTo(16.2f)
+ curveTo(17.048f, 5.5f, 17.655f, 5.5f, 18.13f, 5.539f)
+ curveTo(18.599f, 5.577f, 18.896f, 5.651f, 19.135f, 5.772f)
+ curveTo(19.605f, 6.012f, 19.988f, 6.395f, 20.228f, 6.865f)
+ curveTo(20.349f, 7.104f, 20.423f, 7.401f, 20.461f, 7.87f)
+ curveTo(20.5f, 8.345f, 20.5f, 8.952f, 20.5f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(20.5f, 15.048f, 20.5f, 15.655f, 20.461f, 16.13f)
+ curveTo(20.423f, 16.599f, 20.349f, 16.896f, 20.228f, 17.135f)
+ curveTo(19.988f, 17.605f, 19.605f, 17.988f, 19.135f, 18.228f)
+ curveTo(18.896f, 18.349f, 18.599f, 18.423f, 18.13f, 18.461f)
+ curveTo(17.655f, 18.5f, 17.048f, 18.5f, 16.2f, 18.5f)
+ horizontalLineTo(7.8f)
+ curveTo(6.952f, 18.5f, 6.345f, 18.5f, 5.87f, 18.461f)
+ curveTo(5.401f, 18.423f, 5.104f, 18.349f, 4.865f, 18.228f)
+ curveTo(4.395f, 17.988f, 4.012f, 17.605f, 3.772f, 17.135f)
+ curveTo(3.651f, 16.896f, 3.577f, 16.599f, 3.539f, 16.13f)
+ curveTo(3.5f, 15.655f, 3.5f, 15.048f, 3.5f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(3.5f, 8.952f, 3.5f, 8.345f, 3.539f, 7.87f)
+ curveTo(3.577f, 7.401f, 3.651f, 7.104f, 3.772f, 6.865f)
+ curveTo(4.012f, 6.395f, 4.395f, 6.012f, 4.865f, 5.772f)
+ curveTo(5.104f, 5.651f, 5.401f, 5.577f, 5.87f, 5.539f)
+ curveTo(6.345f, 5.5f, 6.952f, 5.5f, 7.8f, 5.5f)
+ close()
+ }
+ if (tColor != null) {
+ path(fill = SolidColor(tColor)) {
+ moveTo(11.082f, 16f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(9f)
+ verticalLineTo(8f)
+ horizontalLineTo(15f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(12.913f)
+ verticalLineTo(16f)
+ horizontalLineTo(11.082f)
+ close()
+ }
+ }
+ }.build()
+
+ fun buildCard2(mainColor: Color, secondColor: Color, borderColor: Color, tColor: Color?): ImageVector =
+ ImageVector.Builder(
+ name = "KeyCard2",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(fill = SolidColor(mainColor)) {
+ moveTo(2f, 9.8f)
+ curveTo(2f, 8.12f, 2f, 7.28f, 2.327f, 6.638f)
+ curveTo(2.615f, 6.074f, 3.074f, 5.615f, 3.638f, 5.327f)
+ curveTo(4.28f, 5f, 5.12f, 5f, 6.8f, 5f)
+ horizontalLineTo(15.2f)
+ curveTo(16.88f, 5f, 17.72f, 5f, 18.362f, 5.327f)
+ curveTo(18.927f, 5.615f, 19.385f, 6.074f, 19.673f, 6.638f)
+ curveTo(20f, 7.28f, 20f, 8.12f, 20f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(20f, 15.88f, 20f, 16.72f, 19.673f, 17.362f)
+ curveTo(19.385f, 17.927f, 18.927f, 18.385f, 18.362f, 18.673f)
+ curveTo(17.72f, 19f, 16.88f, 19f, 15.2f, 19f)
+ horizontalLineTo(6.8f)
+ curveTo(5.12f, 19f, 4.28f, 19f, 3.638f, 18.673f)
+ curveTo(3.074f, 18.385f, 2.615f, 17.927f, 2.327f, 17.362f)
+ curveTo(2f, 16.72f, 2f, 15.88f, 2f, 14.2f)
+ verticalLineTo(9.8f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(6.8f, 5.5f)
+ horizontalLineTo(15.2f)
+ curveTo(16.048f, 5.5f, 16.655f, 5.5f, 17.13f, 5.539f)
+ curveTo(17.599f, 5.577f, 17.896f, 5.651f, 18.135f, 5.772f)
+ curveTo(18.605f, 6.012f, 18.988f, 6.395f, 19.228f, 6.865f)
+ curveTo(19.349f, 7.104f, 19.423f, 7.401f, 19.461f, 7.87f)
+ curveTo(19.5f, 8.345f, 19.5f, 8.952f, 19.5f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(19.5f, 15.048f, 19.5f, 15.655f, 19.461f, 16.13f)
+ curveTo(19.423f, 16.599f, 19.349f, 16.896f, 19.228f, 17.135f)
+ curveTo(18.988f, 17.605f, 18.605f, 17.988f, 18.135f, 18.228f)
+ curveTo(17.896f, 18.349f, 17.599f, 18.423f, 17.13f, 18.461f)
+ curveTo(16.655f, 18.5f, 16.048f, 18.5f, 15.2f, 18.5f)
+ horizontalLineTo(6.8f)
+ curveTo(5.952f, 18.5f, 5.345f, 18.5f, 4.87f, 18.461f)
+ curveTo(4.401f, 18.423f, 4.104f, 18.349f, 3.865f, 18.228f)
+ curveTo(3.395f, 17.988f, 3.012f, 17.605f, 2.772f, 17.135f)
+ curveTo(2.651f, 16.896f, 2.577f, 16.599f, 2.539f, 16.13f)
+ curveTo(2.5f, 15.655f, 2.5f, 15.048f, 2.5f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(2.5f, 8.952f, 2.5f, 8.345f, 2.539f, 7.87f)
+ curveTo(2.577f, 7.401f, 2.651f, 7.104f, 2.772f, 6.865f)
+ curveTo(3.012f, 6.395f, 3.395f, 6.012f, 3.865f, 5.772f)
+ curveTo(4.104f, 5.651f, 4.401f, 5.577f, 4.87f, 5.539f)
+ curveTo(5.345f, 5.5f, 5.952f, 5.5f, 6.8f, 5.5f)
+ close()
+ }
+ if (tColor != null) {
+ path(fill = SolidColor(tColor)) {
+ moveTo(10.082f, 16f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(8f)
+ verticalLineTo(8f)
+ horizontalLineTo(14f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(11.913f)
+ verticalLineTo(16f)
+ horizontalLineTo(10.082f)
+ close()
+ }
+ }
+ path(fill = SolidColor(secondColor)) {
+ moveTo(17.535f, 5f)
+ curveTo(19.272f, 5f, 20.142f, 5f, 20.806f, 5.327f)
+ curveTo(21.389f, 5.615f, 21.864f, 6.073f, 22.162f, 6.638f)
+ curveTo(22.5f, 7.279f, 22.5f, 8.12f, 22.5f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(22.5f, 15.88f, 22.5f, 16.721f, 22.162f, 17.362f)
+ curveTo(21.864f, 17.927f, 21.389f, 18.385f, 20.806f, 18.673f)
+ curveTo(20.142f, 19f, 19.272f, 19f, 17.535f, 19f)
+ horizontalLineTo(16.5f)
+ curveTo(18.238f, 19f, 19.107f, 19f, 19.771f, 18.673f)
+ curveTo(20.355f, 18.385f, 20.83f, 17.927f, 21.127f, 17.362f)
+ curveTo(21.465f, 16.721f, 21.465f, 15.88f, 21.465f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(21.465f, 8.12f, 21.465f, 7.279f, 21.127f, 6.638f)
+ curveTo(20.83f, 6.073f, 20.355f, 5.615f, 19.771f, 5.327f)
+ curveTo(19.107f, 5f, 18.238f, 5f, 16.5f, 5f)
+ horizontalLineTo(17.535f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(21.831f, 7.153f)
+ curveTo(21.89f, 7.342f, 21.934f, 7.57f, 21.959f, 7.871f)
+ curveTo(21.999f, 8.345f, 22f, 8.951f, 22f, 9.8f)
+ verticalLineTo(14.2f)
+ lineTo(21.995f, 15.305f)
+ curveTo(21.99f, 15.621f, 21.979f, 15.892f, 21.959f, 16.129f)
+ curveTo(21.934f, 16.429f, 21.89f, 16.657f, 21.831f, 16.846f)
+ curveTo(21.873f, 16.648f, 21.902f, 16.438f, 21.921f, 16.213f)
+ curveTo(21.965f, 15.687f, 21.966f, 15.032f, 21.966f, 14.2f)
+ verticalLineTo(9.8f)
+ lineTo(21.96f, 8.679f)
+ curveTo(21.955f, 8.345f, 21.943f, 8.05f, 21.921f, 7.787f)
+ curveTo(21.902f, 7.561f, 21.873f, 7.351f, 21.831f, 7.153f)
+ close()
+ }
+ }.build()
+
+ fun buildCard3(
+ mainColor: Color,
+ secondColor: Color,
+ thirdColor: Color,
+ borderColor: Color,
+ tColor: Color?,
+ ): ImageVector = ImageVector.Builder(
+ name = "KeyCard3",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(fill = SolidColor(mainColor)) {
+ moveTo(0f, 9.8f)
+ curveTo(0f, 8.12f, 0f, 7.28f, 0.327f, 6.638f)
+ curveTo(0.615f, 6.074f, 1.074f, 5.615f, 1.638f, 5.327f)
+ curveTo(2.28f, 5f, 3.12f, 5f, 4.8f, 5f)
+ horizontalLineTo(13.2f)
+ curveTo(14.88f, 5f, 15.72f, 5f, 16.362f, 5.327f)
+ curveTo(16.927f, 5.615f, 17.385f, 6.074f, 17.673f, 6.638f)
+ curveTo(18f, 7.28f, 18f, 8.12f, 18f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(18f, 15.88f, 18f, 16.72f, 17.673f, 17.362f)
+ curveTo(17.385f, 17.927f, 16.927f, 18.385f, 16.362f, 18.673f)
+ curveTo(15.72f, 19f, 14.88f, 19f, 13.2f, 19f)
+ horizontalLineTo(4.8f)
+ curveTo(3.12f, 19f, 2.28f, 19f, 1.638f, 18.673f)
+ curveTo(1.074f, 18.385f, 0.615f, 17.927f, 0.327f, 17.362f)
+ curveTo(0f, 16.72f, 0f, 15.88f, 0f, 14.2f)
+ verticalLineTo(9.8f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(4.8f, 5.5f)
+ horizontalLineTo(13.2f)
+ curveTo(14.048f, 5.5f, 14.655f, 5.5f, 15.13f, 5.539f)
+ curveTo(15.599f, 5.577f, 15.896f, 5.651f, 16.135f, 5.772f)
+ curveTo(16.605f, 6.012f, 16.988f, 6.395f, 17.228f, 6.865f)
+ curveTo(17.349f, 7.104f, 17.423f, 7.401f, 17.461f, 7.87f)
+ curveTo(17.5f, 8.345f, 17.5f, 8.952f, 17.5f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(17.5f, 15.048f, 17.5f, 15.655f, 17.461f, 16.13f)
+ curveTo(17.423f, 16.599f, 17.349f, 16.896f, 17.228f, 17.135f)
+ curveTo(16.988f, 17.605f, 16.605f, 17.988f, 16.135f, 18.228f)
+ curveTo(15.896f, 18.349f, 15.599f, 18.423f, 15.13f, 18.461f)
+ curveTo(14.655f, 18.5f, 14.048f, 18.5f, 13.2f, 18.5f)
+ horizontalLineTo(4.8f)
+ curveTo(3.952f, 18.5f, 3.345f, 18.5f, 2.87f, 18.461f)
+ curveTo(2.401f, 18.423f, 2.104f, 18.349f, 1.865f, 18.228f)
+ curveTo(1.395f, 17.988f, 1.012f, 17.605f, 0.772f, 17.135f)
+ curveTo(0.651f, 16.896f, 0.577f, 16.599f, 0.539f, 16.13f)
+ curveTo(0.5f, 15.655f, 0.5f, 15.048f, 0.5f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(0.5f, 8.952f, 0.5f, 8.345f, 0.539f, 7.87f)
+ curveTo(0.577f, 7.401f, 0.651f, 7.104f, 0.772f, 6.865f)
+ curveTo(1.012f, 6.395f, 1.395f, 6.012f, 1.865f, 5.772f)
+ curveTo(2.104f, 5.651f, 2.401f, 5.577f, 2.87f, 5.539f)
+ curveTo(3.345f, 5.5f, 3.952f, 5.5f, 4.8f, 5.5f)
+ close()
+ }
+ if (tColor != null) {
+ path(fill = SolidColor(tColor)) {
+ moveTo(8.082f, 16f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(6f)
+ verticalLineTo(8f)
+ horizontalLineTo(12f)
+ verticalLineTo(9.635f)
+ horizontalLineTo(9.913f)
+ verticalLineTo(16f)
+ horizontalLineTo(8.082f)
+ close()
+ }
+ }
+ path(fill = SolidColor(secondColor)) {
+ moveTo(16.035f, 5f)
+ curveTo(17.772f, 5f, 18.642f, 5f, 19.306f, 5.327f)
+ curveTo(19.889f, 5.615f, 20.364f, 6.073f, 20.662f, 6.638f)
+ curveTo(21f, 7.279f, 21f, 8.12f, 21f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(21f, 15.88f, 21f, 16.721f, 20.662f, 17.362f)
+ curveTo(20.364f, 17.927f, 19.889f, 18.385f, 19.306f, 18.673f)
+ curveTo(18.642f, 19f, 17.772f, 19f, 16.035f, 19f)
+ horizontalLineTo(15f)
+ curveTo(16.738f, 19f, 17.607f, 19f, 18.271f, 18.673f)
+ curveTo(18.855f, 18.385f, 19.33f, 17.927f, 19.627f, 17.362f)
+ curveTo(19.965f, 16.721f, 19.965f, 15.88f, 19.965f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(19.965f, 8.12f, 19.965f, 7.279f, 19.627f, 6.638f)
+ curveTo(19.33f, 6.073f, 18.855f, 5.615f, 18.271f, 5.327f)
+ curveTo(17.607f, 5f, 16.738f, 5f, 15f, 5f)
+ horizontalLineTo(16.035f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(20.331f, 7.153f)
+ curveTo(20.39f, 7.342f, 20.434f, 7.57f, 20.459f, 7.871f)
+ curveTo(20.499f, 8.345f, 20.5f, 8.951f, 20.5f, 9.8f)
+ verticalLineTo(14.2f)
+ lineTo(20.495f, 15.305f)
+ curveTo(20.49f, 15.621f, 20.479f, 15.892f, 20.459f, 16.129f)
+ curveTo(20.434f, 16.429f, 20.39f, 16.657f, 20.331f, 16.846f)
+ curveTo(20.373f, 16.648f, 20.402f, 16.438f, 20.421f, 16.213f)
+ curveTo(20.465f, 15.687f, 20.466f, 15.032f, 20.466f, 14.2f)
+ verticalLineTo(9.8f)
+ lineTo(20.46f, 8.679f)
+ curveTo(20.455f, 8.345f, 20.443f, 8.05f, 20.421f, 7.787f)
+ curveTo(20.402f, 7.561f, 20.373f, 7.351f, 20.331f, 7.153f)
+ close()
+ }
+ path(fill = SolidColor(thirdColor)) {
+ moveTo(19.035f, 5f)
+ curveTo(20.772f, 5f, 21.642f, 5f, 22.306f, 5.327f)
+ curveTo(22.889f, 5.615f, 23.364f, 6.073f, 23.662f, 6.638f)
+ curveTo(24f, 7.279f, 24f, 8.12f, 24f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(24f, 15.88f, 24f, 16.721f, 23.662f, 17.362f)
+ curveTo(23.364f, 17.927f, 22.889f, 18.385f, 22.306f, 18.673f)
+ curveTo(21.642f, 19f, 20.772f, 19f, 19.035f, 19f)
+ horizontalLineTo(18f)
+ curveTo(19.738f, 19f, 20.607f, 19f, 21.271f, 18.673f)
+ curveTo(21.855f, 18.385f, 22.33f, 17.927f, 22.627f, 17.362f)
+ curveTo(22.965f, 16.721f, 22.965f, 15.88f, 22.965f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(22.965f, 8.12f, 22.965f, 7.279f, 22.627f, 6.638f)
+ curveTo(22.33f, 6.073f, 21.855f, 5.615f, 21.271f, 5.327f)
+ curveTo(20.607f, 5f, 19.738f, 5f, 18f, 5f)
+ horizontalLineTo(19.035f)
+ close()
+ }
+ path(
+ stroke = SolidColor(borderColor),
+ strokeLineWidth = 1f,
+ ) {
+ moveTo(23.331f, 7.153f)
+ curveTo(23.39f, 7.342f, 23.434f, 7.57f, 23.459f, 7.871f)
+ curveTo(23.499f, 8.345f, 23.5f, 8.951f, 23.5f, 9.8f)
+ verticalLineTo(14.2f)
+ curveTo(23.5f, 15.049f, 23.499f, 15.655f, 23.459f, 16.129f)
+ curveTo(23.434f, 16.429f, 23.39f, 16.657f, 23.331f, 16.846f)
+ curveTo(23.373f, 16.648f, 23.402f, 16.438f, 23.421f, 16.213f)
+ curveTo(23.465f, 15.687f, 23.466f, 15.032f, 23.466f, 14.2f)
+ verticalLineTo(9.8f)
+ curveTo(23.466f, 8.968f, 23.465f, 8.313f, 23.421f, 7.787f)
+ curveTo(23.402f, 7.561f, 23.373f, 7.351f, 23.331f, 7.153f)
+ close()
+ }
+ }.build()
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt
index 3591920330..86d955f2d9 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt
@@ -22,11 +22,11 @@ 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.flicker
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@@ -58,7 +58,15 @@ fun TangemMessage(
if (messageUM.iconUM != null) {
TangemIcon(
tangemIconUM = messageUM.iconUM,
- modifier = Modifier.size(TangemTheme.dimens2.x8),
+ modifier = Modifier
+ .align(
+ if (messageUM.buttonsUM.isEmpty()) {
+ Alignment.CenterVertically
+ } else {
+ Alignment.Top
+ },
+ )
+ .size(TangemTheme.dimens2.x7),
)
}
},
@@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider if (isInDarkTheme) {
- persistentListOf()
+ persistentListOf(
+ Color(0x1AFFFFFF),
+ Color(0x1AFFFFFF),
+ )
} else {
persistentListOf(
Color(0x0d000000),
@@ -238,9 +244,10 @@ internal fun Modifier.messageEffectBackground(
val isInDarkTheme = LocalIsInDarkTheme.current
val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) }
val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) }
+ val gradientTint = remember { messageEffect.getGradientTint(isInDarkTheme) }
val angle by rememberAnimationAngle(messageEffect.isAnimatable)
- val brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme))
+ val brush = remember { Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) }
val padding = 1.dp.toPx()
return this
@@ -254,14 +261,14 @@ internal fun Modifier.messageEffectBackground(
border(
width = 1.dp,
brush = Brush.sweepGradient(
- colors = messageEffect.getBorderGradient(isInDarkTheme),
+ colors = borderGradientColors,
center = Offset.Infinite,
),
shape = RoundedCornerShape(radius),
)
}
.hazeForegroundEffectTangem(
- style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)),
+ style = HazeStyle(tints = gradientTint),
isBlurEnabled = true,
) {
fallbackTint = HazeTint(
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt
new file mode 100644
index 0000000000..8fd7342071
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt
@@ -0,0 +1,274 @@
+package com.tangem.core.ui.ds.opportunities
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.draw.innerShadow
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.shadow.Shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.CircleShimmer
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.res.LocalIsInDarkTheme
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
+import dev.chrisbanes.haze.HazeStyle
+
+/**
+ * Container that draws a blurred background (from URL or solid color) and
+ * applies a semi‑transparent overlay on top of it, then renders foreground content.
+ *
+ * Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev
+ *
+ * @param icon Background configuration (URL, solid color or none).
+ * @param modifier Modifier applied to the outer container.
+ * @param content Foreground content rendered on top of the overlay.
+ * @param shape Shape used for inner shadow and border (e.g. rounded corners).
+ */
+@Suppress("MagicNumber")
+@Composable
+fun OpportunitiesBG(
+ icon: TangemIconUM,
+ modifier: Modifier = Modifier,
+ shape: Shape = RoundedCornerShape(16.dp),
+ content: @Composable BoxScope.() -> Unit,
+) {
+ val isInDarkTheme = LocalIsInDarkTheme.current
+ val overlayColor = remember(isInDarkTheme) {
+ if (isInDarkTheme) {
+ Color(OVERLAY_DARK)
+ } else {
+ Color.White
+ }
+ }
+
+ Box(modifier = modifier) {
+ BackgroundLayer(icon = icon)
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(shape)
+ .innerShadow(
+ shape = shape,
+ shadow = Shadow(
+ radius = 30.dp,
+ spread = 5.dp,
+ color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f),
+ offset = DpOffset(0.dp, 0.dp),
+ ),
+ )
+ .innerShadow(
+ shape = shape,
+ shadow = Shadow(
+ radius = 100.dp,
+ spread = (-39).dp,
+ color = Color(INNER_SHADOW_COLOR_END).copy(.3f),
+ offset = DpOffset(0.dp, (-56).dp),
+ ),
+ )
+ .innerShadow(
+ shape = shape,
+ shadow = Shadow(
+ radius = 40.dp,
+ spread = (-19).dp,
+ color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f),
+ offset = DpOffset(0.dp, (-16).dp),
+ ),
+ )
+ .drawWithContent {
+ drawRect(color = overlayColor.copy(alpha = .7f))
+ drawContent()
+ val outline = shape.createOutline(size, layoutDirection, this)
+ drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx()))
+ },
+ content = content,
+ )
+ }
+}
+
+@Suppress("CyclomaticComplexMethod")
+@Composable
+private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) {
+ when (icon) {
+ is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius)
+ is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius)
+ is TangemIconUM.Ident -> Unit
+ is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius)
+ is TangemIconUM.Url -> UrlColorBackground(icon.url, blurRadius)
+ }
+}
+
+@Suppress("CyclomaticComplexMethod")
+@Composable
+private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) {
+ when (state) {
+ is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground(
+ color = state.color,
+ blurRadius = blurRadius,
+ )
+ is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground(
+ color = state.color,
+ blurRadius = blurRadius,
+ )
+ is CurrencyIconState.CustomTokenIcon -> SolidColorBackground(
+ color = state.background,
+ blurRadius = blurRadius,
+ )
+ is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius)
+ is CurrencyIconState.CoinIcon -> {
+ state.url?.let {
+ UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
+ } ?: run {
+ ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
+ }
+ }
+ is CurrencyIconState.FiatIcon -> state.url?.let {
+ UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
+ } ?: run {
+ ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
+ }
+ is CurrencyIconState.TokenIcon -> state.url?.let {
+ UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
+ } ?: run {
+ SolidColorBackground(
+ color = state.fallbackBackground,
+ blurRadius = blurRadius,
+ )
+ }
+ CurrencyIconState.Loading -> Unit
+ CurrencyIconState.Locked -> Unit
+ }
+}
+
+@Composable
+private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) {
+ val context = LocalContext.current
+
+ val imageRequest = remember(imageUrl) {
+ if (imageUrl.isNullOrBlank()) {
+ null
+ } else {
+ ImageRequest.Builder(context)
+ .data(imageUrl)
+ .crossfade(true)
+ .build()
+ }
+ }
+
+ if (imageRequest != null) {
+ AsyncImage(
+ model = imageRequest,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .matchParentSize()
+ .scale(SCALE_FACTOR)
+ .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
+ )
+ }
+}
+
+@Composable
+private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) {
+ Image(
+ painter = painterResource(res),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .matchParentSize()
+ .scale(SCALE_FACTOR)
+ .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
+ )
+}
+
+@Composable
+private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) {
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .background(color = color)
+ .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
+ )
+}
+
+@Composable
+private fun BoxScope.UrlColorBackground(url: String, blurRadius: Dp) {
+ SubcomposeAsyncImage(
+ modifier = Modifier
+ .matchParentSize()
+ .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
+ model = ImageRequest.Builder(context = LocalContext.current)
+ .data(url)
+ .crossfade(enable = true)
+ .allowHardware(enable = false)
+ .build(),
+ loading = { CircleShimmer() },
+ error = {
+ Box(
+ modifier = Modifier
+ .background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = CircleShape,
+ ),
+ )
+ },
+ contentDescription = null,
+ )
+}
+
+private const val SCALE_FACTOR = 1.5f
+private const val INNER_SHADOW_COLOR_START = 0x00000000
+private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF
+
+private const val BORDER_COLOR = 0xFFF0F0F0
+private const val OVERLAY_DARK = 0xFF141414
+
+// region Previews
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun OpportunitiesBGPreview() {
+ TangemThemePreview {
+ OpportunitiesBG(
+ modifier = Modifier.size(400.dp),
+ icon = TangemIconUM.Currency(
+ CurrencyIconState.CoinIcon(
+ url = null,
+ fallbackResId = R.drawable.img_solana_22,
+ isGrayscale = false,
+ shouldShowCustomBadge = false,
+ ),
+ ),
+ content = {},
+ )
+ }
+}
+
+// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt
index b1a24c2067..026dc41e70 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt
@@ -16,8 +16,8 @@ import kotlin.math.max
/**
* A custom layout composable that arranges its children in a row with specific layout IDs.
*/
-internal enum class TangemRowLayoutId {
- HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
+enum class TangemRowLayoutId {
+ HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM
}
/**
@@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId {
*/
@Suppress("LongMethod")
@Composable
-internal fun TangemRowContainer(
+fun TangemRowContainer(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
content: @Composable () -> Unit,
@@ -37,6 +37,7 @@ internal fun TangemRowContainer(
val density = LocalDensity.current
val localDirection = LocalLayoutDirection.current
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
+ val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() }
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
@@ -45,7 +46,7 @@ internal fun TangemRowContainer(
content = content,
modifier = modifier,
) { measurables, constraints ->
- val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding
+ val layoutWidth = max(0, constraints.maxWidth - contentStartPadding - contentEndPadding)
val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
@@ -110,6 +111,10 @@ internal fun TangemRowContainer(
layoutId = TangemRowLayoutId.EXTRA_TOP,
constraints = constraints,
)
+ val extraBottomPlaceable = measurables.measure(
+ layoutId = TangemRowLayoutId.EXTRA_BOTTOM,
+ constraints = constraints,
+ )
val mainLayoutHeight = maxOf(
headPlaceable.heightOrZero(),
@@ -124,7 +129,13 @@ internal fun TangemRowContainer(
contentTopPadding
}
- val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
+ val mainContentBottomPadding = if (extraBottomPlaceable != null) {
+ extraBottomPlaceable.heightOrZero() + contentBottomPadding
+ } else {
+ contentBottomPadding
+ }
+
+ val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding
layout(width = constraints.maxWidth, height = layoutHeight) {
extraTopPlaceable?.placeRelative(x = 0, y = 0)
@@ -174,6 +185,11 @@ internal fun TangemRowContainer(
x = layoutWidth - tailPlaceable.width + contentEndPadding,
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
)
+
+ extraBottomPlaceable?.placeRelative(
+ x = 0,
+ y = mainContentTopPadding + mainLayoutHeight + extraContentPadding,
+ )
}
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt
index f5af27f6c1..64d73f459d 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt
@@ -1,22 +1,18 @@
package com.tangem.core.ui.ds.row.header
import android.content.res.Configuration
-import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Icon
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -25,13 +21,13 @@ import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.clickableSingle
-import com.tangem.core.ui.extensions.resolveAnnotatedReference
-import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.ds.row.internal.TangemRowTail
+import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
+import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* UI model for header row component
@@ -40,12 +36,19 @@ import com.tangem.core.ui.test.TokenElementsTestTags
* @param modifier Modifier for the composable
*/
@Composable
-fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) {
+fun TangemHeaderRow(
+ headerRowUM: TangemHeaderRowUM,
+ modifier: Modifier = Modifier,
+ reorderableState: ReorderableLazyListState? = null,
+ isBalanceHidden: Boolean = false,
+) {
TangemHeaderRow(
headTangemIconUM = headerRowUM.startIconUM,
- footerTangemIconRes = headerRowUM.endIconRes,
+ tailUM = headerRowUM.tailUM,
title = headerRowUM.title,
subtitle = headerRowUM.subtitle,
+ isBalanceHidden = isBalanceHidden,
+ reorderableState = reorderableState,
modifier = modifier,
)
}
@@ -56,16 +59,17 @@ fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifie
* @param modifier Modifier for the composable
* @param subtitle Optional subtitle as a TextReference
* @param onItemClick Optional click callback for the row
- * @param footerTangemIconRes Optional drawable resource ID for the footer icon
+ * @param tailUM Optional TailUM for the tail content
* @param titleContent Composable lambda for the title content
* @param headContent Composable lambda for the head content
*/
@Composable
fun TangemHeaderRow(
modifier: Modifier = Modifier,
+ isBalanceHidden: Boolean = false,
subtitle: TextReference? = null,
onItemClick: (() -> Unit)? = null,
- @DrawableRes footerTangemIconRes: Int? = null,
+ tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
titleContent: @Composable (Modifier) -> Unit,
headContent: @Composable (Modifier) -> Unit,
) {
@@ -92,7 +96,7 @@ fun TangemHeaderRow(
) {
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
Text(
- text = wrappedSubtitle.resolveAnnotatedReference(),
+ text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
@@ -102,17 +106,7 @@ fun TangemHeaderRow(
)
}
SpacerWMax()
- AnimatedVisibility(
- visible = footerTangemIconRes != null,
- ) {
- val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) }
- Icon(
- imageVector = ImageVector.vectorResource(id = wrappedIconUM),
- contentDescription = null,
- tint = TangemTheme.colors2.graphic.neutral.secondary,
- modifier = Modifier.size(TangemTheme.dimens2.x4),
- )
- }
+ TangemRowTail(tangemRowTailUM = tailUM)
}
}
@@ -131,9 +125,11 @@ fun TangemHeaderRow(
fun TangemHeaderRow(
title: TextReference,
modifier: Modifier = Modifier,
+ isBalanceHidden: Boolean = false,
subtitle: TextReference? = null,
headTangemIconUM: TangemIconUM? = null,
- @DrawableRes footerTangemIconRes: Int? = null,
+ tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
+ reorderableState: ReorderableLazyListState? = null,
isEnabled: Boolean = false,
onItemClick: (() -> Unit)? = null,
) {
@@ -172,7 +168,7 @@ fun TangemHeaderRow(
) {
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
Text(
- text = wrappedSubtitle.resolveAnnotatedReference(),
+ text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
@@ -182,17 +178,10 @@ fun TangemHeaderRow(
)
}
SpacerWMax()
- AnimatedVisibility(
- visible = footerTangemIconRes != null,
- ) {
- val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) }
- Icon(
- imageVector = ImageVector.vectorResource(id = wrappedIconUM),
- contentDescription = null,
- tint = TangemTheme.colors2.graphic.neutral.secondary,
- modifier = Modifier.size(TangemTheme.dimens2.x4),
- )
- }
+ TangemRowTail(
+ tangemRowTailUM = tailUM,
+ reorderableState = reorderableState,
+ )
}
}
@@ -217,7 +206,25 @@ private class PreviewProvider : PreviewParameterProvider {
startIconUM = TangemIconUM.Currency(
currencyIconState = CurrencyIconState.Locked,
),
- endIconRes = R.drawable.ic_minimize_24,
+ tailUM = TangemRowTailUM.Empty,
+ title = stringReference("Account"),
+ subtitle = stringReference("\$ 42,900.17"),
+ ),
+ TangemHeaderRowUM(
+ id = "1",
+ startIconUM = TangemIconUM.Currency(
+ currencyIconState = CurrencyIconState.Locked,
+ ),
+ tailUM = TangemRowTailUM.Icon(R.drawable.ic_arrow_collapse_24),
+ title = stringReference("Account"),
+ subtitle = stringReference("\$ 42,900.17"),
+ ),
+ TangemHeaderRowUM(
+ id = "1",
+ startIconUM = TangemIconUM.Currency(
+ currencyIconState = CurrencyIconState.Locked,
+ ),
+ tailUM = TangemRowTailUM.Icon(R.drawable.ic_group_drop_24),
title = stringReference("Account"),
subtitle = stringReference("\$ 42,900.17"),
),
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt
index fd69abb660..28c3d44501 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt
@@ -1,9 +1,9 @@
package com.tangem.core.ui.ds.row.header
-import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowUM
+import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
import com.tangem.core.ui.extensions.TextReference
/**
@@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
* @param title Title text reference
* @param subtitle Subtitle text reference (optional)
* @param startIconUM Icon UI model (optional)
- * @param endIconRes Icon UI model (optional)
+ * @param tailUM Tail UI model (optional)
* @param isEnabled Flag indicating if click is enabled
* @param onItemClick Callback for item click (optional)
*/
@@ -23,7 +23,7 @@ data class TangemHeaderRowUM(
val title: TextReference,
val subtitle: TextReference? = null,
val startIconUM: TangemIconUM? = null,
- @DrawableRes val endIconRes: Int? = null,
+ val tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
val isEnabled: Boolean = false,
val onItemClick: (() -> Unit)? = null,
) : TangemRowUM
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt
similarity index 59%
rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt
rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt
index f3416f8a29..913d8b2bea 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTail.kt
@@ -1,7 +1,9 @@
-package com.tangem.core.ui.ds.row.token.internal
+package com.tangem.core.ui.ds.row.internal
+import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
@@ -9,11 +11,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
-import com.tangem.core.ui.R
-import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
@@ -22,37 +23,48 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
-internal fun TokenRowTail(
- tailUM: TangemTokenRowUM.TailUM,
- reorderableTokenListState: ReorderableLazyListState?,
+internal fun TangemRowTail(
+ tangemRowTailUM: TangemRowTailUM,
modifier: Modifier = Modifier,
+ reorderableState: ReorderableLazyListState? = null,
) {
AnimatedContent(
- targetState = tailUM,
+ targetState = tangemRowTailUM,
label = "Update non content fiat block",
- modifier = modifier,
+ modifier = modifier.height(TangemTheme.dimens2.x4),
contentKey = { it::class.java },
) { animatedState ->
val innerModifier = Modifier.padding(start = TangemTheme.dimens2.x2)
when (animatedState) {
- TangemTokenRowUM.TailUM.Empty -> Unit
- is TangemTokenRowUM.TailUM.Draggable -> DraggableImage(
- reorderableTokenListState = reorderableTokenListState,
+ TangemRowTailUM.Empty -> Unit
+ is TangemRowTailUM.Draggable -> DraggableImage(
+ iconRes = animatedState.iconRes,
+ reorderableState = reorderableState,
modifier = innerModifier,
)
- is TangemTokenRowUM.TailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
+ is TangemRowTailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
+ is TangemRowTailUM.Icon -> Icon(
+ imageVector = ImageVector.vectorResource(animatedState.iconRes),
+ contentDescription = null,
+ modifier = modifier,
+ tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant,
+ )
}
}
}
@Composable
-private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) {
+private fun DraggableImage(
+ @DrawableRes iconRes: Int,
+ reorderableState: ReorderableLazyListState?,
+ modifier: Modifier = Modifier,
+) {
Box(
modifier = modifier
.size(size = TangemTheme.dimens2.x6)
.then(
- other = if (reorderableTokenListState != null) {
- Modifier.detectReorder(reorderableTokenListState)
+ other = if (reorderableState != null) {
+ Modifier.detectReorder(reorderableState)
} else {
Modifier
},
@@ -61,7 +73,7 @@ private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?,
contentAlignment = Alignment.Center,
) {
Icon(
- painter = painterResource(id = R.drawable.ic_drag_24),
+ imageVector = ImageVector.vectorResource(iconRes),
tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant,
contentDescription = null,
)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt
new file mode 100644
index 0000000000..841faea488
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/internal/TangemRowTailUM.kt
@@ -0,0 +1,25 @@
+package com.tangem.core.ui.ds.row.internal
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Immutable
+import com.tangem.core.ui.extensions.TextReference
+
+/**
+ * Tail UI model for row components
+ */
+@Immutable
+sealed class TangemRowTailUM {
+ data class Text(
+ val text: TextReference,
+ ) : TangemRowTailUM()
+
+ data class Icon(
+ @DrawableRes val iconRes: Int,
+ ) : TangemRowTailUM()
+
+ data class Draggable(
+ @DrawableRes val iconRes: Int,
+ ) : TangemRowTailUM()
+
+ data object Empty : TangemRowTailUM()
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt
index a0b50fe199..cd2b93a853 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
+import com.tangem.core.ui.ds.row.internal.TangemRowTail
import com.tangem.core.ui.ds.row.token.internal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@@ -30,16 +31,16 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
- * @param tokenRowUM The user model containing the data for the token row.
- * @param isBalanceHidden A boolean indicating whether the balance should be hidden.
- * @param reorderableTokenListState The state of the reorderable lazy list, if applicable.
- * @param modifier The modifier to be applied to the row.
+ * @param tokenRowUM The user model containing the data for the token row.
+ * @param isBalanceHidden A boolean indicating whether the balance should be hidden.
+ * @param reorderableState The state of the reorderable lazy list, if applicable.
+ * @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
- reorderableTokenListState: ReorderableLazyListState?,
+ reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
@@ -52,15 +53,6 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)
- TokenRowPromoBanner(
- promoBannerUM = tokenRowUM.promoBannerUM,
- modifier = Modifier
- .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
- .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
- .padding(horizontal = TangemTheme.dimens2.x3)
- .fillMaxWidth(),
- )
-
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
@@ -77,29 +69,42 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
- TokenRowEndTopContent(
+ TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
+ textStyle = TangemTheme.typography2.bodySemibold16,
+ textColor = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
- TokenRowEndBottomContent(
+ TokenRowEndContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
+ textStyle = TangemTheme.typography2.captionSemibold12,
+ textColor = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
)
- TokenRowTail(
- tailUM = tokenRowUM.tailUM,
- reorderableTokenListState = reorderableTokenListState,
+ TangemRowTail(
+ tangemRowTailUM = tokenRowUM.tailUM,
+ reorderableState = reorderableState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
+
+ TokenRowPromoBanner(
+ promoBannerUM = tokenRowUM.promoBannerUM,
+ modifier = Modifier
+ .layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM)
+ .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
+ .padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2)
+ .fillMaxWidth(),
+ )
},
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
)
@@ -110,18 +115,18 @@ fun TangemTokenRow(
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
- * @param tokenRowUM The user model containing the data for the token row.
- * @param headComponent The composable function representing the head component.
- * @param titleComponent The composable function representing the title component.
- * @param isBalanceHidden A boolean indicating whether the balance should be hidden.
- * @param reorderableTokenListState The state of the reorderable lazy list, if applicable.
- * @param modifier The modifier to be applied to the row.
+ * @param tokenRowUM The user model containing the data for the token row.
+ * @param headComponent The composable function representing the head component.
+ * @param titleComponent The composable function representing the title component.
+ * @param isBalanceHidden A boolean indicating whether the balance should be hidden.
+ * @param reorderableState The state of the reorderable lazy list, if applicable.
+ * @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
- reorderableTokenListState: ReorderableLazyListState?,
+ reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
headComponent: @Composable (Modifier) -> Unit,
titleComponent: @Composable (Modifier) -> Unit,
@@ -159,25 +164,29 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
- TokenRowEndTopContent(
+ TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
+ textStyle = TangemTheme.typography2.bodySemibold16,
+ textColor = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
- TokenRowEndBottomContent(
+ TokenRowEndContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
+ textStyle = TangemTheme.typography2.captionSemibold12,
+ textColor = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
)
- TokenRowTail(
- tailUM = tokenRowUM.tailUM,
- reorderableTokenListState = reorderableTokenListState,
+ TangemRowTail(
+ tangemRowTailUM = tokenRowUM.tailUM,
+ reorderableState = reorderableState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
@@ -218,19 +227,20 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTokenRow_Preview(
- @PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM,
+ @PreviewParameter(TangemTokenRow_PreviewProvider::class) tokenRowUM: TangemTokenRowUM,
) {
TangemThemePreviewRedesign {
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = false,
- reorderableTokenListState = null,
+ reorderableState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}
}
-private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider(
+@Suppress("ClassNaming")
+class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider(
collection = listOf(
TangemTokenRowPreviewData.defaultState,
TangemTokenRowPreviewData.defaultEllipsisState,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt
index 98027ff5dd..c183f9863b 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt
@@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.badge.TangemBadgeUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowUM
+import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -32,7 +33,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
abstract val bottomEndContentUM: EndContentUM
/** Token row tail UM */
- abstract val tailUM: TailUM
+ abstract val tailUM: TangemRowTailUM
/** Promo banner UM */
abstract val promoBannerUM: PromoBannerUM
@@ -54,7 +55,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val topEndContentUM: EndContentUM,
override val bottomEndContentUM: EndContentUM,
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty,
- override val tailUM: TailUM = TailUM.Empty,
+ override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
) : TangemTokenRowUM()
@@ -71,7 +72,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val topEndContentUM: EndContentUM = EndContentUM.Loading
override val bottomEndContentUM: EndContentUM = EndContentUM.Loading
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
- override val tailUM: TailUM = TailUM.Empty
+ override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
}
@@ -84,7 +85,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val headIconUM: TangemIconUM.Currency,
override val titleUM: TitleUM,
override val subtitleUM: SubtitleUM,
- override val tailUM: TailUM,
+ override val tailUM: TangemRowTailUM,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
override val topEndContentUM: EndContentUM = EndContentUM.Empty,
@@ -133,7 +134,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
- val icons: ImmutableList = persistentListOf(),
+ val startIcons: ImmutableList = persistentListOf(),
+ val endIcons: ImmutableList = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
) : EndContentUM()
@@ -153,15 +155,4 @@ sealed class TangemTokenRowUM : TangemRowUM {
data object Empty : PromoBannerUM()
}
-
- @Immutable
- sealed class TailUM {
- data class Text(
- val text: TextReference,
- ) : TailUM()
-
- data object Draggable : TailUM()
-
- data object Empty : TailUM()
- }
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt
index ce68a8771d..fb7b25744f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt
@@ -7,6 +7,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
@@ -123,7 +124,7 @@ internal object TangemTokenRowPreviewData {
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
- tailUM = TangemTokenRowUM.TailUM.Empty,
+ tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
@@ -142,7 +143,7 @@ internal object TangemTokenRowPreviewData {
)
}),
),
- icons = persistentListOf(
+ startIcons = persistentListOf(
TangemIconUM.Icon(R.drawable.ic_staking_mini_10),
TangemIconUM.Icon(R.drawable.ic_attention_12),
TangemIconUM.Icon(R.drawable.ic_error_sync_24),
@@ -150,7 +151,7 @@ internal object TangemTokenRowPreviewData {
),
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
- tailUM = TangemTokenRowUM.TailUM.Empty,
+ tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
@@ -164,7 +165,7 @@ internal object TangemTokenRowPreviewData {
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
- tailUM = TangemTokenRowUM.TailUM.Empty,
+ tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
@@ -178,7 +179,7 @@ internal object TangemTokenRowPreviewData {
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
- tailUM = TangemTokenRowUM.TailUM.Empty,
+ tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
@@ -189,7 +190,7 @@ internal object TangemTokenRowPreviewData {
headIconUM = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
- tailUM = TangemTokenRowUM.TailUM.Draggable,
+ tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
onItemClick = {},
onItemLongClick = {},
)
@@ -202,7 +203,7 @@ internal object TangemTokenRowPreviewData {
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
- tailUM = TangemTokenRowUM.TailUM.Draggable,
+ tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
onItemClick = {},
onItemLongClick = {},
)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt
deleted file mode 100644
index 376e670bda..0000000000
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.tangem.core.ui.ds.row.token.internal
-
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-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.tooling.preview.PreviewParameterProvider
-import com.tangem.core.ui.components.TextShimmer
-import com.tangem.core.ui.components.marketprice.PriceChangeState
-import com.tangem.core.ui.components.text.applyBladeBrush
-import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
-import com.tangem.core.ui.extensions.orMaskWithStars
-import com.tangem.core.ui.extensions.resolveAnnotatedReference
-import com.tangem.core.ui.res.TangemTheme
-import com.tangem.core.ui.res.TangemThemePreviewRedesign
-
-@Composable
-internal fun TokenRowEndBottomContent(
- endContentUM: TangemTokenRowUM.EndContentUM,
- isBalanceHidden: Boolean,
- modifier: Modifier = Modifier,
-) {
- when (endContentUM) {
- is TangemTokenRowUM.EndContentUM.Content -> Content(
- modifier = modifier,
- endContentUM = endContentUM,
- isBalanceHidden = isBalanceHidden,
- )
- TangemTokenRowUM.EndContentUM.Empty -> Unit
- TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
- style = TangemTheme.typography2.captionSemibold12,
- modifier = modifier.width(TangemTheme.dimens2.x10),
- radius = TangemTheme.dimens2.x25,
- )
- }
-}
-
-@Composable
-private fun Content(
- endContentUM: TangemTokenRowUM.EndContentUM.Content,
- isBalanceHidden: Boolean,
- modifier: Modifier = Modifier,
-) {
- Row(
- modifier = modifier,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
- isEnabled = endContentUM.isFlickering,
- textColor = if (endContentUM.isAvailable) {
- TangemTheme.colors2.text.neutral.secondary
- } else {
- TangemTheme.colors2.text.status.disabled
- },
- ),
- )
-
- when (val priceChangeUM = endContentUM.priceChangeUM) {
- is PriceChangeState.Content -> TokenRowPriceChangeContent(
- priceChangeState = priceChangeUM,
- isFlickering = endContentUM.isFlickering,
- isAvailable = endContentUM.isAvailable,
- )
- PriceChangeState.Unknown -> Unit
- }
- }
-}
-
-// region Preview
-@Composable
-@Preview(showBackground = true, widthDp = 360)
-@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
-private fun TokenRowEndBottomContent_Preview(
- @PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
-) {
- TangemThemePreviewRedesign {
- TokenRowEndBottomContent(
- endContentUM = params,
- isBalanceHidden = false,
- )
- }
-}
-
-private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- TangemTokenRowPreviewData.bottomEndContentUM,
- )
-}
-// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt
similarity index 64%
rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt
rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt
index 131497424f..934b33aa8c 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt
@@ -8,15 +8,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+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.TextStyle
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.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.TextShimmer
+import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
@@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
-internal fun TokenRowEndTopContent(
+internal fun TokenRowEndContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
+ textStyle: TextStyle,
+ textColor: Color,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
@@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
+ textStyle = textStyle,
+ textColor = textColor,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
- style = TangemTheme.typography2.bodySemibold16,
- modifier = modifier.width(TangemTheme.dimens2.x18),
+ style = textStyle,
+ modifier = modifier.width(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
@@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent(
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
+ textStyle: TextStyle,
+ textColor: Color,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
@@ -56,14 +65,14 @@ private fun Content(
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
- visible = endContentUM.icons.isNotEmpty(),
+ visible = endContentUM.startIcons.isNotEmpty(),
) {
Row(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
- endContentUM.icons.fastForEach { icon ->
+ endContentUM.startIcons.fastForEach { icon ->
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
@@ -75,11 +84,11 @@ private fun Content(
}
Text(
- modifier = Modifier,
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
+ color = textColor,
+ style = textStyle.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
@@ -88,6 +97,34 @@ private fun Content(
},
),
)
+
+ AnimatedVisibility(
+ visible = endContentUM.endIcons.isNotEmpty(),
+ ) {
+ Row(
+ modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
+ ) {
+ endContentUM.endIcons.fastForEach { icon ->
+ Icon(
+ modifier = Modifier.size(TangemTheme.dimens2.x3),
+ painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
+ tint = icon.tintReference(),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+
+ when (val priceChangeUM = endContentUM.priceChangeUM) {
+ is PriceChangeState.Content -> TokenRowPriceChangeContent(
+ priceChangeState = priceChangeUM,
+ isFlickering = endContentUM.isFlickering,
+ isAvailable = endContentUM.isAvailable,
+ )
+ PriceChangeState.Unknown -> Unit
+ }
}
}
@@ -95,13 +132,15 @@ private fun Content(
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
-private fun TokenRowEndTopContent_Preview(
+private fun TokenRowEndContent_Preview(
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
- TokenRowEndTopContent(
+ TokenRowEndContent(
endContentUM = params,
isBalanceHidden = false,
+ textColor = TangemTheme.colors2.text.neutral.primary,
+ textStyle = TangemTheme.typography2.captionSemibold12,
)
}
}
@@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview(
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
- TangemTokenRowPreviewData.topEndContentUM,
+ TangemTokenRowPreviewData.bottomEndContentUM,
)
}
// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt
index ef635a0a8e..052c994ac7 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt
@@ -24,6 +24,12 @@ internal fun RowScope.TokenRowPriceChangeContent(
isFlickering: Boolean,
isAvailable: Boolean = true,
) {
+ val color = when (priceChangeState.type) {
+ PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
+ PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
+ PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.tertiary
+ }
+
AnimatedContent(
targetState = priceChangeState.type,
label = "Update the price change's arrow",
@@ -39,11 +45,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
},
),
),
- tint = when (animatedType) {
- PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
- PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
- PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary
- },
+ tint = color,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x3),
)
@@ -61,7 +63,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) {
- TangemTheme.colors2.text.neutral.secondary
+ color
} else {
TangemTheme.colors2.text.status.disabled
},
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt
index 93845dea18..013204a20f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt
@@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
-import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -19,6 +16,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
+import com.tangem.core.ui.ds.badge.*
+import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@@ -40,55 +39,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C
LaunchedEffect(promoBannerUM) {
promoBannerUM.onPromoShown()
}
- val bgColor = TangemTheme.colors.control.default
- Column(modifier = modifier) {
+ val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen
+ Column(
+ modifier = modifier,
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.shape_triangular),
+ contentDescription = null,
+ tint = bgColor,
+ modifier = Modifier.padding(start = TangemTheme.dimens2.x5),
+ )
Row(
modifier = Modifier
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
.clickable(onClick = promoBannerUM.onPromoBannerClick)
- .padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
- .fillMaxWidth(),
+ .padding(
+ start = TangemTheme.dimens2.x2_5,
+ end = TangemTheme.dimens2.x0_5,
+ top = TangemTheme.dimens2.x0_5,
+ bottom = TangemTheme.dimens2.x0_5,
+ ),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
contentDescription = null,
- tint = TangemTheme.colors.icon.accent,
+ tint = TangemTheme.colors2.markers.textGreen,
modifier = Modifier
- .padding(end = TangemTheme.dimens2.x2)
- .size(TangemTheme.dimens2.x4),
+ .padding(vertical = TangemTheme.dimens2.x0_5)
+ .size(TangemTheme.dimens2.x3),
)
Text(
text = promoBannerUM.title.resolveReference(),
- style = TangemTheme.typography2.captionSemibold12,
- color = TangemTheme.colors2.text.neutral.secondary,
+ style = TangemTheme.typography2.captionSemibold11,
+ color = TangemTheme.colors2.markers.textGreen,
modifier = Modifier
- .weight(1f)
- .padding(end = TangemTheme.dimens2.x2),
+ .padding(vertical = TangemTheme.dimens2.x0_5),
)
- Icon(
- painter = painterResource(id = R.drawable.ic_close_24),
- contentDescription = null,
- tint = TangemTheme.colors2.text.neutral.secondary,
- modifier = Modifier
- .size(TangemTheme.dimens2.x4)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = ripple(bounded = false),
- onClick = { promoBannerUM.onCloseClick() },
- ),
- )
- }
- Box(
- modifier = Modifier.fillMaxWidth(),
- contentAlignment = Alignment.Center,
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_rectangle_bottom),
- contentDescription = null,
- tint = bgColor,
- modifier = Modifier
- .size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
+ TangemBadge(
+ size = TangemBadgeSize.X4,
+ shape = TangemBadgeShape.Rounded,
+ color = TangemBadgeColor.Green,
+ type = TangemBadgeType.Tinted,
+ tangemIconUM = TangemIconUM.Icon(R.drawable.ic_close_24),
+ iconPosition = TangemBadgeIconPosition.None,
+ onClick = promoBannerUM.onCloseClick,
)
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt
index 67e1989fae..54d39c5104 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt
@@ -45,7 +45,7 @@ fun TangemTab(
val backgroundColor = if (isChecked) {
TangemTheme.colors2.tabs.backgroundPrimary
} else {
- TangemTheme.colors2.tabs.textPrimary
+ TangemTheme.colors2.tabs.backgroundSecondary
}
val textColor = if (isChecked) {
TangemTheme.colors2.tabs.textPrimary
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt
index b96dd8d788..c9dca9b4fc 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt
@@ -2,22 +2,26 @@ package com.tangem.core.ui.ds.topbar
import android.content.res.Configuration
import androidx.annotation.DrawableRes
-import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.TextStyle
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
@@ -30,11 +34,9 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
* @param title The title text to be displayed in the center of the top bar.
* @param modifier Modifier to be applied to the top bar.
* @param subtitle Optional subtitle text to be displayed below the title.
- * @param startIconRes Optional drawable resource ID for the start icon.
- * @param onStartContentClick Optional click action for the start icon.
- * @param endIconRes Optional drawable resource ID for the end icon.
- * @param onEndContentClick Optional click action for the end icon.
- * @param isGhostButtons Flag to determine if ghost button styling should be applied.
+ * @param startActionUM Optional action data for the start action icon.
+ * @param endActionUM Optional action data for the end action icon.
+ * @param titleIconRes Optional drawable resource ID for the icon to be displayed next to the title.
*
[REDACTED_AUTHOR]
*/
@@ -43,63 +45,109 @@ fun TangemTopBar(
modifier: Modifier = Modifier,
title: TextReference? = null,
subtitle: TextReference? = null,
- @DrawableRes startIconRes: Int? = null,
- onStartContentClick: (() -> Unit)? = null,
- @DrawableRes endIconRes: Int? = null,
- onEndContentClick: (() -> Unit)? = null,
+ startActionUM: TangemTopBarActionUM? = null,
+ endActionUM: TangemTopBarActionUM? = null,
@DrawableRes titleIconRes: Int? = null,
- titleStyle: TextStyle = TangemTheme.typography2.headingSemibold17,
- isGhostButtons: Boolean = false,
) {
- TangemTopBarInner(
+ TangemTopBar(
+ title = title,
+ subtitle = subtitle,
+ titleIconRes = titleIconRes,
modifier = modifier,
- content = {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
- ) {
- TangemTopBarTitle(title = title, titleIconRes = titleIconRes, titleStyle = titleStyle)
- AnimatedVisibility(
- visible = subtitle != null,
- label = "Subtitle Visibility",
- ) {
- val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
- Text(
- text = wrappedSubtitle.resolveAnnotatedReference(),
- color = TangemTheme.colors2.text.neutral.secondary,
- style = TangemTheme.typography2.bodyRegular15,
- textAlign = TextAlign.Center,
- maxLines = 1,
- )
- }
- }
- },
- startContent = if (startIconRes != null) {
- { TangemTopBarIcon(iconRes = startIconRes) }
+ startContent = if (startActionUM != null) {
+ { TangemTopBarActionContent(startActionUM) }
} else {
null
},
- onStartContentClick = onStartContentClick,
- endContent = if (endIconRes != null) {
- { TangemTopBarIcon(iconRes = endIconRes) }
+ endContent = if (endActionUM != null) {
+ { TangemTopBarActionContent(endActionUM) }
} else {
null
},
- onEndContentClick = onEndContentClick,
- isGhostButtons = isGhostButtons,
)
}
+/**
+ * A top bar composable that displays a title and optional start and end icons.
+ * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
+ *
+ * @param title The title text to be displayed in the center of the top bar.
+ * @param modifier Modifier to be applied to the top bar.
+ * @param subtitle Optional subtitle text to be displayed below the title.
+ *
+[REDACTED_AUTHOR]
+ */
@Composable
-private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?, titleStyle: TextStyle) {
+fun TangemTopBar(
+ modifier: Modifier = Modifier,
+ title: TextReference? = null,
+ subtitle: TextReference? = null,
+ @DrawableRes titleIconRes: Int? = null,
+ startContent: @Composable (() -> Unit)? = null,
+ endContent: @Composable (() -> Unit)? = null,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens2.x16)
+ .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3),
+ ) {
+ AnimatedVisibility(
+ visible = startContent != null,
+ modifier = Modifier.align(Alignment.CenterStart),
+ label = "Start Content Visibility",
+ ) {
+ startContent?.invoke()
+ }
+
+ Column(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
+ ) {
+ TangemTopBarTitle(title = title, titleIconRes = titleIconRes)
+ AnimatedVisibility(
+ visible = subtitle != null,
+ label = "Subtitle Visibility",
+ ) {
+ val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
+ Text(
+ text = wrappedSubtitle.resolveAnnotatedReference(),
+ color = TangemTheme.colors2.text.neutral.secondary,
+ style = TangemTheme.typography2.bodyRegular15,
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ )
+ }
+ }
+ AnimatedVisibility(
+ visible = endContent != null,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ label = "End Content Visibility",
+ ) {
+ endContent?.invoke()
+ }
+ }
+}
+
+@Composable
+private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?) {
AnimatedVisibility(
visible = title != null,
label = "Title Visibility",
+ enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
) {
val wrappedTitle = remember(this) { requireNotNull(title) }
Row(
- horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(
+ space = TangemTheme.dimens2.x1,
+ alignment = Alignment.CenterHorizontally,
+ ),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
@@ -120,7 +168,7 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
Text(
text = wrappedTitle.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.primary,
- style = titleStyle,
+ style = TangemTheme.typography2.headingSemibold17,
textAlign = TextAlign.Center,
maxLines = 1,
)
@@ -129,12 +177,29 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
}
@Composable
-private fun TangemTopBarIcon(@DrawableRes iconRes: Int) {
+fun TangemTopBarActionContent(
+ actionUM: TangemTopBarActionUM,
+ modifier: Modifier = Modifier,
+ iconSize: Dp = TangemTheme.dimens2.x8,
+) {
+ val background = lerp(
+ start = Color.Transparent,
+ stop = TangemTheme.colors2.button.backgroundSecondary,
+ fraction = actionUM.ghostModeProgress,
+ )
+ val padding = (TangemTheme.dimens2.x10 - iconSize) / 2
Icon(
- imageVector = ImageVector.vectorResource(id = iconRes),
+ imageVector = ImageVector.vectorResource(id = actionUM.iconRes),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
- modifier = Modifier.fillMaxSize(),
+ modifier = modifier
+ .size(TangemTheme.dimens2.x10)
+ .clip(CircleShape)
+ .conditional(actionUM.isActionable) { background(background) }
+ .conditionalCompose(actionUM.isActionable && actionUM.onClick != null) {
+ clickableSingle(onClick = requireNotNull(actionUM.onClick))
+ }
+ .padding(padding),
)
}
@@ -147,13 +212,10 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param
TangemTopBar(
title = params.title,
subtitle = params.subtitle,
- startIconRes = params.startIconRes,
- endIconRes = params.endIconRes,
titleIconRes = params.titleIconRes,
- isGhostButtons = params.isGhostButtons,
- onStartContentClick = {},
- onEndContentClick = {},
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
+ startContent = params.startActionUM?.let { { TangemTopBarActionContent(it) } },
+ endContent = params.endActionUM?.let { { TangemTopBarActionContent(it) } },
)
}
}
@@ -161,10 +223,9 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param
private class TangemTopBarPreviewData(
val title: TextReference? = null,
val subtitle: TextReference? = null,
- val isGhostButtons: Boolean = false,
val titleIconRes: Int? = null,
- val startIconRes: Int? = null,
- val endIconRes: Int? = null,
+ val startActionUM: TangemTopBarActionUM? = null,
+ val endActionUM: TangemTopBarActionUM? = null,
)
private class PreviewProvider : PreviewParameterProvider {
@@ -172,41 +233,79 @@ private class PreviewProvider : PreviewParameterProvider Unit)? = null,
+ @param:FloatRange(0.0, 1.0) val ghostModeProgress: Float = 0f,
+)
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt
deleted file mode 100644
index e445ea4a30..0000000000
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.tangem.core.ui.ds.topbar
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.platform.LocalDensity
-import com.tangem.core.ui.extensions.clickableSingle
-import com.tangem.core.ui.extensions.conditional
-import com.tangem.core.ui.extensions.conditionalCompose
-import com.tangem.core.ui.res.TangemTheme
-
-/**
- * Internal top bar composable that arranges optional start, center, and end content.
- * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
- *
- * @param modifier Modifier to be applied to the top bar.
- * @param content Center content of the top bar.
- * @param startContent Optional start content of the top bar.
- * @param onStartContentClick Optional click action for the start content.
- * @param endContent Optional end content of the top bar.
- * @param onEndContentClick Optional click action for the end content.
- * @param isGhostButtons Flag to determine if ghost button styling should be applied.
- *
-[REDACTED_AUTHOR]
- */
-@Composable
-internal fun TangemTopBarInner(
- modifier: Modifier = Modifier,
- content: (@Composable () -> Unit)? = null,
- startContent: (@Composable () -> Unit)? = null,
- onStartContentClick: (() -> Unit)? = null,
- endContent: (@Composable () -> Unit)? = null,
- onEndContentClick: (() -> Unit)? = null,
- isGhostButtons: Boolean = false,
-) {
- val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() }
- Box(
- modifier = modifier
- .height(TangemTheme.dimens2.x16 + statusBarHeight)
- .fillMaxWidth()
- .padding(top = statusBarHeight)
- .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3),
- ) {
- val iconModifier = Modifier
- .size(TangemTheme.dimens2.x10)
- .clip(RoundedCornerShape(TangemTheme.dimens2.x25))
- .conditionalCompose(isGhostButtons) {
- background(TangemTheme.colors2.button.backgroundSecondary)
- }
-
- AnimatedVisibility(
- visible = startContent != null,
- modifier = Modifier.align(Alignment.CenterStart),
- label = "Start Content Visibility",
- ) {
- Box(
- modifier = iconModifier
- .conditional(onStartContentClick != null) {
- clickableSingle { onStartContentClick?.invoke() }
- }
- .conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
- ) {
- startContent?.invoke()
- }
- }
-
- AnimatedVisibility(
- visible = content != null,
- modifier = Modifier.align(Alignment.Center),
- ) {
- content?.invoke()
- }
-
- AnimatedVisibility(
- visible = endContent != null,
- modifier = Modifier.align(Alignment.CenterEnd),
- label = "End Content Visibility",
- ) {
- Box(
- modifier = iconModifier
- .conditional(onEndContentClick != null) {
- clickableSingle { onEndContentClick?.invoke() }
- }
- .conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
- ) {
- endContent?.invoke()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt
new file mode 100644
index 0000000000..1d91d223b5
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt
@@ -0,0 +1,126 @@
+package com.tangem.core.ui.ds.topbar.collapsing
+
+import android.content.res.Configuration
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+
+@Composable
+fun TangemCollapsingTopBar(
+ state: TangemCollapsingAppBarState,
+ collapsingPart: @Composable () -> Unit,
+ body: @Composable () -> Unit,
+) {
+ Layout(
+ modifier = Modifier.fillMaxSize(),
+ content = {
+ collapsingPart()
+ body()
+ },
+ ) { measurables, constraints ->
+
+ val collapsingConstraints = constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ )
+ val collapsingPlaceable = measurables[0].measure(collapsingConstraints)
+
+ val bodyConstraints = constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ maxHeight = (constraints.maxHeight - collapsingConstraints.minHeight).coerceAtLeast(0),
+ )
+ val bodyPlaceable = measurables[1].measure(bodyConstraints)
+
+ val minHeight = 0.dp.roundToPx()
+ val maxHeight = collapsingPlaceable.height + minHeight
+
+ val offset = state.heightOffset.roundToInt().coerceAtLeast(-maxHeight)
+
+ val width = max(
+ collapsingPlaceable.width,
+ bodyPlaceable.width,
+ ).coerceIn(constraints.minWidth, constraints.maxWidth)
+ val height = max(
+ collapsingPlaceable.height,
+ bodyPlaceable.height,
+ ).coerceIn(constraints.minHeight, constraints.maxHeight)
+
+ layout(width = width, height = height) {
+ bodyPlaceable.placeRelative(0, collapsingPlaceable.height + offset)
+ collapsingPlaceable.placeRelative(0, offset)
+ }
+ }
+}
+
+/**
+ * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
+ *
+ * @property state The state of the collapsing app bar.
+ * @property snapAnimationSpec The animation spec used for snapping the app bar to its collapsed or
+ * expanded state after a fling. If null, no snapping will occur.
+ * @property flingAnimationSpec The decay animation spec used for fling gestures.
+ * If null, fling gestures will not be handled.
+ * @property nestedScrollConnection Nested scroll connection
+ */
+@Stable
+data class TangemCollapsingAppBarBehavior(
+ val state: TangemCollapsingAppBarState,
+ val snapAnimationSpec: AnimationSpec?,
+ val flingAnimationSpec: DecayAnimationSpec?,
+ val nestedScrollConnection: NestedScrollConnection,
+)
+
+// region Preview
+@Composable
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun TangemCollapsingTopBar_Preview() {
+ TangemThemePreviewRedesign {
+ val collapsingHeight = 200.dp
+ val behavior = rememberTangemExitUntilCollapsedScrollBehavior(
+ expandedHeight = collapsingHeight,
+ )
+ TangemCollapsingTopBar(
+ state = behavior.state,
+ collapsingPart = {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(collapsingHeight)
+ .background(Color.Red),
+ )
+ },
+ body = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Blue)
+ .nestedScroll(behavior.nestedScrollConnection)
+ .verticalScroll(rememberScrollState()),
+ )
+ },
+ )
+ }
+}
+// endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt
new file mode 100644
index 0000000000..c3e18a6df6
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt
@@ -0,0 +1,211 @@
+package com.tangem.core.ui.ds.topbar.collapsing
+
+import androidx.compose.animation.core.*
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
+import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection
+import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState
+import com.tangem.core.ui.utils.toPx
+import kotlin.math.abs
+import kotlin.math.absoluteValue
+
+/**
+ * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
+ * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state
+ * based on the current collapsed fraction and scroll direction.
+ *
+ * @param expandedHeight The height of the app bar when it is fully expanded.
+ * @param partialCollapsedHeight The height of the app bar when it is partially collapsed.
+ * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the
+ * user stops scrolling. If null, no snapping will occur.
+ * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar.
+ * If null, no fling behavior will occur.
+ */
+@Composable
+fun rememberTangemExitUntilCollapsedScrollBehavior(
+ expandedHeight: Dp = -Int.MAX_VALUE.dp,
+ partialCollapsedHeight: Dp = expandedHeight,
+ snapAnimationSpec: AnimationSpec? = spring(),
+ flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(),
+): TangemCollapsingAppBarBehavior {
+ val topBarState = rememberTangemCollapsingAppBarState(
+ heightOffsetLimit = -expandedHeight.toPx(),
+ partialHeightLimit = partialCollapsedHeight.toPx(),
+ )
+ return exitUntilCollapsedScrollBehavior(
+ state = topBarState,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ )
+}
+
+/**
+ * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
+ * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state
+ * based on the current collapsed fraction and scroll direction.
+ *
+ * @param state The state of the collapsing app bar, which controls the height offset and scroll behavior.
+ * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the
+ * user stops scrolling. If null, no snapping will occur.
+ * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar.
+ * If null, no fling behavior will occur.
+ */
+@Composable
+private fun exitUntilCollapsedScrollBehavior(
+ state: TangemCollapsingAppBarState = rememberTangemCollapsingAppBarState(),
+ snapAnimationSpec: AnimationSpec? = spring(),
+ flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(),
+): TangemCollapsingAppBarBehavior {
+ val nestedScrollConnection = remember(state) {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val dy = available.y
+
+ val consume = if (dy < 0) {
+ state.direction = TopBapScrollDirection.Collapsing
+ state.dispatchRawDelta(dy)
+ } else {
+ 0f
+ }
+
+ return Offset(0f, consume)
+ }
+
+ override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
+ val dy = available.y
+
+ val consume = if (dy > 0) {
+ state.direction = TopBapScrollDirection.Expanding
+ state.dispatchRawDelta(dy)
+ } else {
+ state.direction = TopBapScrollDirection.Collapsing
+ 0f
+ }
+
+ return Offset(0f, consume)
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ val superConsumed = super.onPostFling(consumed, available)
+ return superConsumed + settleAppBar(
+ state = state,
+ velocity = available.y,
+ flingAnimationSpec = flingAnimationSpec,
+ snapAnimationSpec = snapAnimationSpec,
+ )
+ }
+ }
+ }
+
+ return remember(state, nestedScrollConnection, snapAnimationSpec, flingAnimationSpec) {
+ TangemCollapsingAppBarBehavior(
+ state = state,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ nestedScrollConnection = nestedScrollConnection,
+ )
+ }
+}
+
+@Composable
+fun Modifier.snapToExitUntilCollapsed(behavior: TangemCollapsingAppBarBehavior): Modifier {
+ return draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ behavior.state.heightOffset += delta
+ },
+ onDragStopped = { velocity ->
+ settleAppBar(
+ state = behavior.state,
+ velocity = velocity,
+ flingAnimationSpec = behavior.flingAnimationSpec,
+ snapAnimationSpec = behavior.snapAnimationSpec,
+ )
+ },
+ )
+}
+
+/**
+ * Settles the app bar to either fully collapsed or fully expanded state
+ * based on the current collapsed fraction and scroll direction.
+ */
+@Suppress("MagicNumber", "CyclomaticComplexMethod")
+private suspend fun settleAppBar(
+ state: TangemCollapsingAppBarState,
+ velocity: Float,
+ flingAnimationSpec: DecayAnimationSpec?,
+ snapAnimationSpec: AnimationSpec?,
+ snapCollapseThreshold: Float = 0.3f,
+ snapExpandThreshold: Float = 0.7f,
+): Velocity {
+ val partialLimit = state.heightOffsetLimit + state.partialHeightLimit
+ var remainingVelocity = velocity
+
+ // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+ // and just return Zero Velocity.
+ // Note that we don't check for 0f due to float precision with the collapsedFraction
+ // calculation.
+ if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+ return Velocity.Zero
+ }
+
+ // Fling
+ if (flingAnimationSpec != null && velocity.absoluteValue > 1f) {
+ var lastValue = 0f
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = velocity,
+ ).animateDecay(flingAnimationSpec) {
+ val delta = value - lastValue
+ val initialHeightOffset = state.heightOffset
+
+ val availableDelta = partialLimit - initialHeightOffset
+
+ state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) {
+ (initialHeightOffset + delta).coerceAtLeast(partialLimit)
+ } else {
+ initialHeightOffset + delta
+ }
+
+ val consumed = abs(initialHeightOffset - state.heightOffset)
+ lastValue = value
+ remainingVelocity = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(maxOf(delta, availableDelta) - consumed) > 0.5f) this.cancelAnimation()
+ }
+ }
+ // Snap
+ if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) {
+ AnimationState(initialValue = state.heightOffset).animateTo(
+ when (state.direction) {
+ TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
+ partialLimit
+ } else {
+ 0f
+ }
+ TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
+ 0f
+ } else {
+ partialLimit
+ }
+ TopBapScrollDirection.Idle -> 0f
+ },
+ animationSpec = snapAnimationSpec,
+ ) {
+ state.heightOffset = value
+ }
+ }
+ return Velocity(0f, remainingVelocity)
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt
new file mode 100644
index 0000000000..a5d1a8892d
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt
@@ -0,0 +1,142 @@
+package com.tangem.core.ui.ds.topbar.collapsing.entity
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState.Companion.Saver
+import kotlin.math.absoluteValue
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * State of the collapsing top app bar.
+ * It contains the current height offset, the limits for collapsing and expanding, and the scroll direction.
+ *
+ * @property initialHeightOffset The initial height offset of the app bar. Default is 0f.
+ * @property heightOffsetLimit The height offset limit for full collapse.
+ * @property partialHeightLimit The height offset limit for partial collapse. Default is the same as [heightOffsetLimit]
+ */
+@Stable
+class TangemCollapsingAppBarState(
+ val initialHeightOffset: Float = 0f,
+ val heightOffsetLimit: Float = 0f,
+ val partialHeightLimit: Float = heightOffsetLimit,
+) : ScrollableState {
+
+ private val _heightOffset = mutableFloatStateOf(initialHeightOffset)
+ private var deferredConsumption: Float = 0f
+
+ /**
+ * The current height offset of the app bar.
+ * This value is updated as the user scrolls, and is constrained between [heightOffsetLimit] and 0f.
+ */
+ var heightOffset: Float
+ get() = _heightOffset.floatValue
+ set(newOffset) {
+ _heightOffset.floatValue =
+ newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
+ }
+
+ /**
+ * The fraction of the app bar that is collapsed, calculated as the ratio of [heightOffset] to [heightOffsetLimit].
+ */
+ val collapsedFraction: Float
+ get() =
+ if (heightOffsetLimit != 0f) {
+ heightOffset / heightOffsetLimit
+ } else {
+ 0f
+ }
+
+ /**
+ * The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle.
+ */
+ var direction: TopBapScrollDirection = TopBapScrollDirection.Idle
+
+ private val scrollableState = ScrollableState { value ->
+ val consume = if (value < 0) {
+ max(heightOffsetLimit - heightOffset, value)
+ } else {
+ min(0f - heightOffset, value)
+ }
+
+ val current = consume + deferredConsumption
+ val currentInt = current.toInt()
+
+ if (current.absoluteValue > 0) {
+ heightOffset += currentInt
+ deferredConsumption = current - currentInt
+ }
+
+ consume
+ }
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
+
+ /**
+ *
+ */
+ suspend fun collapse() {
+ AnimationState(initialValue = heightOffset).animateTo(
+ targetValue = heightOffsetLimit + partialHeightLimit,
+ animationSpec = tween(),
+ ) {
+ heightOffset = value
+ }
+ }
+
+ override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) =
+ scrollableState.scroll(scrollPriority, block)
+
+ override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta)
+
+ companion object {
+ /** The default [Saver] implementation for [TangemCollapsingAppBarState]. */
+ val Saver: Saver =
+ listSaver(
+ save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) },
+ restore = { state ->
+ TangemCollapsingAppBarState(
+ heightOffsetLimit = state[0],
+ partialHeightLimit = state[2],
+ initialHeightOffset = state[1],
+ )
+ },
+ )
+ }
+}
+
+/**
+ * Remembers and saves the state of the collapsing top app bar across recompositions and configuration changes.
+ */
+@Composable
+fun rememberTangemCollapsingAppBarState(
+ heightOffsetLimit: Float = -Float.MAX_VALUE,
+ partialHeightLimit: Float = -Float.MAX_VALUE,
+ initialHeightOffset: Float = 0f,
+): TangemCollapsingAppBarState {
+ return rememberSaveable(saver = Saver) {
+ TangemCollapsingAppBarState(
+ initialHeightOffset = initialHeightOffset,
+ partialHeightLimit = partialHeightLimit,
+ heightOffsetLimit = heightOffsetLimit,
+ )
+ }
+}
+
+/**
+ * The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle.
+ */
+enum class TopBapScrollDirection {
+ Collapsing, Expanding, Idle
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/UserInteractionTracking.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/UserInteractionTracking.kt
new file mode 100644
index 0000000000..14adf10f6b
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/UserInteractionTracking.kt
@@ -0,0 +1,40 @@
+package com.tangem.core.ui.extensions
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.input.pointer.pointerInput
+
+val LocalUserInteractionTracker = staticCompositionLocalOf { null }
+
+interface UserInteractionTracker {
+ fun onUserInteraction()
+}
+
+fun Modifier.trackUserInteraction(tracker: UserInteractionTracker?): Modifier {
+ if (tracker == null) return this
+
+ return this
+ .pointerInput(Unit) {
+ awaitEachGesture {
+ val down = awaitFirstDown(requireUnconsumed = false)
+
+ if (down.pressed) {
+ tracker.onUserInteraction()
+ }
+
+ waitForUpOrCancellation()
+ }
+ }
+ .onPreviewKeyEvent { event ->
+ if (event.type == KeyEventType.KeyDown) {
+ tracker.onUserInteraction()
+ }
+ false
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt
index cae9546835..89aaa014aa 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt
@@ -1,5 +1,6 @@
package com.tangem.core.ui.format.bigdecimal
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
@@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
import com.tangem.utils.extensions.isNotWhitespace
import java.math.BigDecimal
import java.math.RoundingMode
+import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
@@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull(
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
}
+open class BigDecimalCryptoFormatStyled(
+ val symbol: String,
+ val decimals: Int,
+ val spanStyleReference: SpanStyleReference,
+ val locale: Locale = Locale.getDefault(),
+ val shouldIgnoreSymbolPosition: Boolean = false,
+) : BigDecimalFormatStyled {
+
+ override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
+}
+
// == Initializers ==
fun BigDecimalFormatScope.crypto(
@@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto(
)
}
+fun BigDecimalFormatScope.cryptoStyled(
+ symbol: String,
+ decimals: Int,
+ spanStyleReference: SpanStyleReference,
+ locale: Locale = Locale.getDefault(),
+): BigDecimalCryptoFormatStyled {
+ return BigDecimalCryptoFormatStyled(
+ symbol = symbol,
+ decimals = decimals,
+ spanStyleReference = spanStyleReference,
+ locale = locale,
+ )
+}
+
+fun BigDecimalFormatScope.cryptoStyled(
+ cryptoCurrency: CryptoCurrency,
+ spanStyleReference: SpanStyleReference,
+ ignoreSymbolPosition: Boolean = false,
+ locale: Locale = Locale.getDefault(),
+): BigDecimalCryptoFormatStyled {
+ return BigDecimalCryptoFormatStyled(
+ symbol = cryptoCurrency.symbol,
+ decimals = cryptoCurrency.decimals,
+ spanStyleReference = spanStyleReference,
+ shouldIgnoreSymbolPosition = ignoreSymbolPosition,
+ locale = locale,
+ )
+}
+
// == Formatters ==
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
@@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
}
}
+fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) =
+ BigDecimalFormatStyled { value ->
+ if (shouldIgnoreSymbolPosition) {
+ val formatter = NumberFormat.getInstance(locale).apply {
+ maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
+ minimumFractionDigits = 2
+ isGroupingUsed = true
+ roundingMode = RoundingMode.HALF_UP
+ }
+
+ val formattedAmount = formatter.format(value)
+
+ val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
+ val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
+
+ combinedReference(
+ stringReference(formattedAmount.take(separatorIndex)),
+ styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
+ stringReference(NON_BREAKING_SPACE + symbol),
+ )
+ } else {
+ val formatter = NumberFormat.getCurrencyInstance(locale).apply {
+ currency = usdCurrency
+ maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
+ minimumFractionDigits = 2
+ isGroupingUsed = true
+ roundingMode = RoundingMode.HALF_UP
+ }
+
+ val formattedAmount = formatter.format(value)
+ .replaceFiatSymbolWithCrypto(
+ fiatCurrencySymbol = usdCurrency.getSymbol(locale),
+ cryptoCurrencySymbol = symbol,
+ )
+
+ val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
+ val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
+
+ combinedReference(
+ stringReference(formattedAmount.take(separatorIndex)),
+ styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
+ )
+ }
+ }
+
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
val formatter = if (value.isMoreThanThreshold()) {
NumberFormat.getCurrencyInstance(locale).apply {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt
index 5c41281344..a1f6549b2b 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt
@@ -1,9 +1,11 @@
package com.tangem.core.ui.format.bigdecimal
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
import com.tangem.utils.StringsSigns.TILDE_SIGN
import java.math.BigDecimal
import java.math.RoundingMode
+import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
@@ -15,8 +17,16 @@ open class BigDecimalFiatFormat(
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
}
-// == Initializers ==
+open class BigDecimalFiatFormatStyled(
+ val fiatCurrencyCode: String,
+ val fiatCurrencySymbol: String,
+ val spanStyleReference: SpanStyleReference,
+ val locale: Locale = Locale.getDefault(),
+) : BigDecimalFormatStyled {
+ override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
+}
+//region == Initializers ==
fun BigDecimalFormatScope.fiat(
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
@@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat(
)
}
-// == Formatters ==
+fun BigDecimalFormatScope.fiat(
+ fiatCurrencyCode: String,
+ fiatCurrencySymbol: String,
+ spanStyleReference: SpanStyleReference,
+ locale: Locale = Locale.getDefault(),
+): BigDecimalFiatFormatStyled {
+ return BigDecimalFiatFormatStyled(
+ fiatCurrencyCode = fiatCurrencyCode,
+ fiatCurrencySymbol = fiatCurrencySymbol,
+ spanStyleReference = spanStyleReference,
+ locale = locale,
+ )
+}
+// endregion == Formatters ==
/**
* Formats fiat amount with default precision.
@@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat {
}
}
+fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value ->
+ val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
+
+ val formatter = NumberFormat.getCurrencyInstance(locale).apply {
+ currency = formatterCurrency
+ maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
+ minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
+ roundingMode = RoundingMode.HALF_UP
+ }
+
+ val formattingAmount = if (value.isLessThanThreshold()) {
+ FIAT_FORMAT_THRESHOLD
+ } else {
+ value
+ }
+
+ val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
+ val formattedAmount = formatter.format(formattingAmount)
+ .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
+
+ val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
+
+ val wholePart = formattedAmount.take(separatorIndex)
+ val fractionalPart = formattedAmount.drop(separatorIndex)
+
+ combinedReference(
+ if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY,
+ stringReference(wholePart),
+ styledStringReference(fractionalPart, spanStyleReference),
+ )
+}
+
/**
* Formats fiat amount with default precision and adds tilde sign
*/
diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt
index c685db7f1d..f7a87efe6a 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt
@@ -1,17 +1,27 @@
package com.tangem.core.ui.format.bigdecimal
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.stringReference
import java.math.BigDecimal
interface BigDecimalFormatScope {
- companion object { val Empty = object : BigDecimalFormatScope {} }
+ companion object {
+ val Empty = object : BigDecimalFormatScope {}
+ }
}
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
+fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope
+
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
return BigDecimalFormatScope.Empty.block()(this)
}
+inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference {
+ return BigDecimalFormatScope.Empty.block()(this)
+}
+
inline fun BigDecimal?.format(
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
block: BigDecimalFormatScope.() -> BigDecimalFormat,
@@ -20,10 +30,26 @@ inline fun BigDecimal?.format(
return BigDecimalFormatScope.Empty.block()(this)
}
+inline fun BigDecimal?.formatStyled(
+ fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
+ block: BigDecimalFormatScope.() -> BigDecimalFormatStyled,
+): TextReference {
+ if (this == null) return stringReference(fallbackString)
+ return BigDecimalFormatScope.Empty.block()(this)
+}
+
fun BigDecimal?.format(
format: BigDecimalFormat,
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
): String {
if (this == null) return fallbackString
return format(this)
+}
+
+fun BigDecimal?.format(
+ format: BigDecimalFormatStyled,
+ fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
+): TextReference {
+ if (this == null) return stringReference(fallbackString)
+ return format(this)
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt
index 5a34d92702..5e5ec69b89 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt
@@ -10,11 +10,7 @@ internal class DefaultHapticManager(
override fun perform(effect: TangemHapticEffect) {
when (effect) {
- is TangemHapticEffect.View -> {
- effect.androidHapticFeedbackCode?.let {
- ViewCompat.performHapticFeedback(view, it)
- }
- }
+ is TangemHapticEffect.View -> performViewEffect(effect)
is TangemHapticEffect.OneTime -> {
if (vibratorHapticManager != null) {
vibratorHapticManager.performOneTime(effect)
@@ -29,4 +25,20 @@ internal class DefaultHapticManager(
}
}
}
+
+ private fun performViewEffect(effect: TangemHapticEffect.View) {
+ val code = effect.androidHapticFeedbackCode ?: return
+ if (ViewCompat.performHapticFeedback(view, code)) return
+
+ val fallbackCode = VIEW_FALLBACKS[effect]?.androidHapticFeedbackCode ?: return
+ ViewCompat.performHapticFeedback(view, fallbackCode)
+ }
+
+ private companion object {
+ val VIEW_FALLBACKS = mapOf(
+ TangemHapticEffect.View.ClockTick to TangemHapticEffect.View.KeyboardPress,
+ TangemHapticEffect.View.Confirm to TangemHapticEffect.View.ContextClick,
+ TangemHapticEffect.View.Reject to TangemHapticEffect.View.LongPress,
+ )
+ }
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt
index 04801d50d7..9b81ff94d3 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt
@@ -16,9 +16,11 @@ object TangemColorPalette {
val Dark4 = Color(0xFF3B3B3B)
val Dark5 = Color(0xFF303030)
val Dark6 = Color(0xFF1E1E1E)
+ val Dark7 = Color(0xFF171717)
// endregion Dark
// region Dark Alpha
+ val Dark_05 = Color(0x0D1E1E1E)
val Dark_10 = Color(0x1A1E1E1E)
val Dark_20 = Color(0x331E1E1E)
val Dark_30 = Color(0x4D1E1E1E)
@@ -58,20 +60,45 @@ object TangemColorPalette {
val DarkGreen = Color(0xFF06311F)
// endregion Green
- // region Blue
+ // region Azure
val Azure = Color(0xFF0099FF)
- // endregion Blue
+ val Azure_50 = Color(0x800099FF)
+ val Azure_10 = Color(0x1A0099FF)
+ // endregion Azure
- // region Red
+ // region Amaranth
val Amaranth = Color(0xFFFF3333)
+ val Amaranth_50 = Color(0x80FF3333)
+ val Amaranth_20 = Color(0x33FF3333)
+ val Amaranth_10 = Color(0x1AFF3333)
+ // endregion Amaranth
+
+ // region Flamingo
val Flamingo = Color(0xFFFF5B5B)
- // endregion Red
+ val Flamingo_50 = Color(0x80FF5B5B)
+ val Flamingo_20 = Color(0x33FF5B5B)
+ val Flamingo_10 = Color(0x1AFF5B5B)
+ // endregion Flamingo
// region Yellow
val Tangerine = Color(0xFFFFB71B)
val Mustard = Color(0xFFFDDE55)
// endregion Yellow
+ // region Emerald
+ val Emerald = Color(0xFF34DF12)
+ val Emerald_50 = Color(0x8034DF12)
+ val Emerald_20 = Color(0x3334DF12)
+ val Emerald_10 = Color(0x1A34DF12)
+ // endregion Emerald
+
+ // region Eucalyptus
+ val Eucalyptus = Color(0xFF0C9F3D)
+ val Eucalyptus_50 = Color(0x800C9F3D)
+ val Eucalyptus_20 = Color(0x330C9F3D)
+ val Eucalyptus_10 = Color(0x1A0C9F3D)
+ // endregion Eucalyptus
+
// region Overlay
val Overlay1 = Color(0x66000000)
val Overlay2 = Color(0xB2000000)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
index c3d2a23e2a..61f297696c 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
@@ -22,6 +22,7 @@ class TangemColors2 internal constructor(
val skeleton: Skeleton,
val markers: Markers,
val tabs: Tabs,
+ val contextMenu: ContextMenu,
) {
@Stable
@@ -326,21 +327,32 @@ class TangemColors2 internal constructor(
class Border internal constructor(
val neutral: Neutral,
val status: Status,
+ walletIcon: Color,
) {
+ var walletIcon by mutableStateOf(walletIcon)
+ private set
@Stable
class Neutral internal constructor(
primary: Color,
secondary: Color,
+ tertiary: Color,
+ quaternary: Color,
) {
var primary by mutableStateOf(primary)
private set
var secondary by mutableStateOf(secondary)
private set
+ var tertiary by mutableStateOf(tertiary)
+ private set
+ var quaternary by mutableStateOf(quaternary)
+ private set
fun update(other: Neutral) {
primary = other.primary
secondary = other.secondary
+ tertiary = other.tertiary
+ quaternary = other.quaternary
}
}
@@ -367,6 +379,7 @@ class TangemColors2 internal constructor(
fun update(other: Border) {
neutral.update(other.neutral)
status.update(other.status)
+ walletIcon = other.walletIcon
}
}
@@ -448,24 +461,36 @@ class TangemColors2 internal constructor(
@Stable
class Markers internal constructor(
- backgroundSolidGray: Color,
- backgroundDisabled: Color,
- backgroundSolidBlue: Color,
- textGray: Color,
textDisabled: Color,
- iconGray: Color,
iconDisabled: Color,
+ backgroundDisabled: Color,
+ textGray: Color,
+ iconGray: Color,
borderGray: Color,
- backgroundTintedBlue: Color,
+ backgroundSolidGray: Color,
+ backgroundTintedGray: Color,
textBlue: Color,
+ iconBlue: Color,
+ borderTintedBlue: Color,
+ backgroundSolidBlue: Color,
+ backgroundTintedBlue: Color,
+ textRed: Color,
+ iconRed: Color,
+ borderTintedRed: Color,
backgroundSolidRed: Color,
backgroundTintedRed: Color,
- iconBlue: Color,
- iconRed: Color,
- textRed: Color,
- backgroundTintedGray: Color,
- borderTintedBlue: Color,
- borderTintedRed: Color,
+ textGreen: Color,
+ iconGreen: Color,
+ borderTintedGreen: Color,
+ borderSolidColor: Color,
+ backgroundTintedGreen: Color,
+ backgroundSolidGreen: Color,
+ textGreenAlt: Color,
+ iconGreenAlt: Color,
+ borderTintedGreenAlt: Color,
+ borderSolidColorAlt: Color,
+ backgroundTintedGreenAlt: Color,
+ backgroundSolidGreenAlt: Color,
) {
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
private set
@@ -504,6 +529,32 @@ class TangemColors2 internal constructor(
var borderTintedRed by mutableStateOf(borderTintedRed)
private set
+ var textGreen by mutableStateOf(textGreen)
+ private set
+ var iconGreen by mutableStateOf(iconGreen)
+ private set
+ var borderTintedGreen by mutableStateOf(borderTintedGreen)
+ private set
+ var borderSolidColor by mutableStateOf(borderSolidColor)
+ private set
+ var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen)
+ private set
+ var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen)
+ private set
+ var textGreenAlt by mutableStateOf(textGreenAlt)
+ private set
+ var iconGreenAlt by mutableStateOf(iconGreenAlt)
+ private set
+ var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt)
+ private set
+ var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt)
+ private set
+
+ var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt)
+ private set
+ var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt)
+ private set
+
fun update(other: Markers) {
backgroundSolidGray = other.backgroundSolidGray
backgroundDisabled = other.backgroundDisabled
@@ -523,6 +574,18 @@ class TangemColors2 internal constructor(
backgroundTintedGray = other.backgroundTintedGray
borderTintedBlue = other.borderTintedBlue
borderTintedRed = other.borderTintedRed
+ textGreen = other.textGreen
+ iconGreen = other.iconGreen
+ borderTintedGreen = other.borderTintedGreen
+ borderSolidColor = other.borderSolidColor
+ backgroundTintedGreen = other.backgroundTintedGreen
+ backgroundSolidGreen = other.backgroundSolidGreen
+ textGreenAlt = other.textGreenAlt
+ iconGreenAlt = other.iconGreenAlt
+ borderTintedGreenAlt = other.borderTintedGreenAlt
+ borderSolidColorAlt = other.borderSolidColorAlt
+ backgroundTintedGreenAlt = other.backgroundTintedGreenAlt
+ backgroundSolidGreenAlt = other.backgroundSolidGreenAlt
}
}
@@ -562,6 +625,18 @@ class TangemColors2 internal constructor(
}
}
+ @Stable
+ class ContextMenu internal constructor(
+ background: Color,
+ ) {
+ var background by mutableStateOf(background)
+ private set
+
+ fun update(other: ContextMenu) {
+ background = other.background
+ }
+ }
+
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
@@ -575,5 +650,6 @@ class TangemColors2 internal constructor(
skeleton.update(other.skeleton)
markers.update(other.markers)
tabs.update(other.tabs)
+ contextMenu.update(other.contextMenu)
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt
index efa104abf2..ad14b94dbf 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt
@@ -79,12 +79,15 @@ private fun lightThemeColors2(): TangemColors2 {
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Light3,
secondary = TangemColorPalette.Light5,
+ tertiary = TangemColorPalette.Light_10,
+ quaternary = TangemColorPalette.Dark_10,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
+ walletIcon = TangemColorPalette.Dark_10,
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
@@ -122,8 +125,8 @@ private fun lightThemeColors2(): TangemColors2 {
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.White,
level2 = TangemColorPalette.Light1V2,
- level3 = TangemColorPalette.Light1V2,
- level4 = TangemColorPalette.White,
+ level3 = TangemColorPalette.White,
+ level4 = TangemColorPalette.Light1V2,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Dark6,
@@ -154,16 +157,28 @@ private fun lightThemeColors2(): TangemColors2 {
iconGray = TangemColorPalette.Dark1,
iconDisabled = TangemColorPalette.Light2,
borderGray = TangemColorPalette.Light3,
- backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
+ backgroundTintedBlue = TangemColorPalette.Azure_10,
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
- backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
+ backgroundTintedRed = TangemColorPalette.Amaranth_10,
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Amaranth,
textRed = TangemColorPalette.Amaranth,
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
- borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
- borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
+ borderTintedBlue = TangemColorPalette.Azure_10,
+ borderTintedRed = TangemColorPalette.Amaranth_10,
+ textGreen = TangemColorPalette.Emerald,
+ iconGreen = TangemColorPalette.Emerald,
+ borderTintedGreen = TangemColorPalette.Emerald_10,
+ borderSolidColor = TangemColorPalette.Emerald_50,
+ backgroundTintedGreen = TangemColorPalette.Emerald_10,
+ backgroundSolidGreen = TangemColorPalette.Emerald,
+ textGreenAlt = TangemColorPalette.Eucalyptus,
+ iconGreenAlt = TangemColorPalette.Eucalyptus,
+ borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
+ borderSolidColorAlt = TangemColorPalette.Eucalyptus_50,
+ backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
+ backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus,
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Light2,
@@ -174,6 +189,9 @@ private fun lightThemeColors2(): TangemColors2 {
backgroundTertiary = TangemColorPalette.White,
backgroundQuaternary = TangemColorPalette.Dark_20,
)
+ val contextMenu = TangemColors2.ContextMenu(
+ background = TangemColorPalette.Dark_05,
+ )
return TangemColors2(
text = text,
graphic = graphic,
@@ -187,6 +205,7 @@ private fun lightThemeColors2(): TangemColors2 {
skeleton = skeleton,
markers = markers,
tabs = tabs,
+ contextMenu = contextMenu,
)
}
@@ -229,12 +248,15 @@ private fun darkThemeColors2(): TangemColors2 {
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Dark4,
secondary = TangemColorPalette.Dark4,
+ tertiary = TangemColorPalette.Light_10,
+ quaternary = TangemColorPalette.Light_10,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
+ walletIcon = TangemColorPalette.Light_10,
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
@@ -270,8 +292,8 @@ private fun darkThemeColors2(): TangemColors2 {
borderPrimary = TangemColorPalette.Light4,
)
val surface = TangemColors2.Surface(
- level1 = TangemColorPalette.Dark6,
- level2 = TangemColorPalette.Black,
+ level1 = TangemColorPalette.Black,
+ level2 = TangemColorPalette.Dark7,
level3 = TangemColorPalette.Dark6,
level4 = TangemColorPalette.Dark5,
)
@@ -304,7 +326,7 @@ private fun darkThemeColors2(): TangemColors2 {
iconGray = TangemColorPalette.Dark2,
iconDisabled = TangemColorPalette.Dark5,
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
- backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
+ backgroundTintedBlue = TangemColorPalette.Azure_10,
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
@@ -312,8 +334,20 @@ private fun darkThemeColors2(): TangemColors2 {
iconRed = TangemColorPalette.Flamingo,
textRed = TangemColorPalette.Flamingo,
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
- borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
- borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
+ borderTintedBlue = TangemColorPalette.Azure_10,
+ borderTintedRed = TangemColorPalette.Amaranth_10,
+ textGreen = TangemColorPalette.Emerald,
+ iconGreen = TangemColorPalette.Emerald,
+ borderTintedGreen = TangemColorPalette.Emerald_10,
+ borderSolidColor = TangemColorPalette.Emerald_50,
+ backgroundTintedGreen = TangemColorPalette.Emerald_10,
+ backgroundSolidGreen = TangemColorPalette.Emerald,
+ textGreenAlt = TangemColorPalette.Emerald,
+ iconGreenAlt = TangemColorPalette.Emerald,
+ borderTintedGreenAlt = TangemColorPalette.Emerald_10,
+ borderSolidColorAlt = TangemColorPalette.Emerald_50,
+ backgroundTintedGreenAlt = TangemColorPalette.Emerald_10,
+ backgroundSolidGreenAlt = TangemColorPalette.Emerald,
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Dark4,
@@ -324,6 +358,9 @@ private fun darkThemeColors2(): TangemColors2 {
backgroundTertiary = TangemColorPalette.Light_10,
backgroundQuaternary = TangemColorPalette.Light_10,
)
+ val contextMenu = TangemColors2.ContextMenu(
+ background = TangemColorPalette.Light_10,
+ )
return TangemColors2(
text = text,
graphic = graphic,
@@ -337,5 +374,6 @@ private fun darkThemeColors2(): TangemColors2 {
skeleton = skeleton,
markers = markers,
tabs = tabs,
+ contextMenu = contextMenu,
)
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt
index d1c1e10b32..cff7ef913b 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt
@@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemeRedesign
/**
* Interface representing a Compose screen with common theming and content composition properties.
@@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView(
uiDependencies = uiDependencies,
overrideSystemBarColors = overrideSystemBarColors,
) {
- ScreenContent(modifier = screenModifier)
+ TangemThemeRedesign {
+ ScreenContent(modifier = screenModifier)
+ }
}
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt
new file mode 100644
index 0000000000..600ad9af39
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt
@@ -0,0 +1,30 @@
+package com.tangem.core.ui.shader
+
+class GlossyShader : TangemShader {
+ override val sksl: String =
+ """
+// The MIT License
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+uniform float uTime;
+uniform vec3 uResolution;
+
+vec4 main( vec2 fragCoord )
+{
+ float mr = min(uResolution.x, uResolution.y);
+ vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr;
+
+ float d = -uTime * 0.5;
+ float a = 0.0;
+ for (float i = 0.0; i < 8.0; ++i) {
+ a += cos(i - d - a * uv.x);
+ d += sin(uv.y * i + a);
+ }
+ d += uTime * 0.5;
+ vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
+ col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5);
+ return vec4(col,1.0);
+}
+ """
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt
new file mode 100644
index 0000000000..05ade74d32
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt
@@ -0,0 +1,193 @@
+@file:Suppress("MagicNumber")
+package com.tangem.core.ui.shader
+
+import androidx.compose.ui.graphics.Color
+import com.tangem.core.ui.shader.runtime.RuntimeEffect
+
+/**
+ * A shader that creates a colorful, flowing "northern lights" effect.
+ * @param colors The colors to display. The last provided color acts like a "background"
+ * @param speed Adjust the speed of the movement
+ * @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs
+ *
+[REDACTED_AUTHOR]
+ */
+class NorthernLightsMeshGradientShader(
+ colors: Array,
+ speed: Float = 1f,
+ scale: Float = 2f,
+) : TangemShader {
+
+ private val colorCount = colors.size
+ private val colorUniforms = colors.flatMap {
+ listOf(it.red, it.green, it.blue)
+ }.toTypedArray().toFloatArray()
+ private val ambientUniform = FloatArray(3)
+
+ init {
+ recomputeAmbient()
+ }
+
+ override val sksl = """
+uniform float uTime;
+uniform vec3 uResolution;
+uniform vec3 uAmbient;
+
+const int MAX_COLORS = $colorCount;
+uniform vec3 uColor[MAX_COLORS];
+
+// Simplex 3D Noise
+// by Ian McEwan, Ashima Arts
+// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
+//
+vec4 permute(vec4 x) {
+ return mod(((x * 34.0) + 1.0) * x, 289.0);
+}
+vec4 taylorInvSqrt(vec4 r) {
+ return 1.79284291400159 - 0.85373472095314 * r;
+}
+
+float snoise(vec3 v) {
+ const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
+
+ // First corner
+ vec3 i = floor(v + dot(v, C.yyy));
+ vec3 x0 = v - i + dot(i, C.xxx);
+
+ // Other corners
+ vec3 g = step(x0.yzx, x0.xyz);
+ vec3 l = 1.0 - g;
+ vec3 i1 = min(g.xyz, l.zxy);
+ vec3 i2 = max(g.xyz, l.zxy);
+
+ // x0 = x0 - 0. + 0.0 * C
+ vec3 x1 = x0 - i1 + 1.0 * C.xxx;
+ vec3 x2 = x0 - i2 + 2.0 * C.xxx;
+ vec3 x3 = x0 - 1. + 3.0 * C.xxx;
+
+ // Permutations
+ i = mod(i, 289.0);
+ vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
+
+ // Gradients
+ // ( N*N points uniformly over a square, mapped onto an octahedron.)
+ float n_ = 1.0 / 7.0; // N=7
+ vec3 ns = n_ * D.wyz - D.xzx;
+
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N)
+
+ vec4 x_ = floor(j * ns.z);
+ vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
+
+ vec4 x = x_ * ns.x + ns.yyyy;
+ vec4 y = y_ * ns.x + ns.yyyy;
+ vec4 h = 1.0 - abs(x) - abs(y);
+
+ vec4 b0 = vec4(x.xy, y.xy);
+ vec4 b1 = vec4(x.zw, y.zw);
+
+ vec4 s0 = floor(b0) * 2.0 + 1.0;
+ vec4 s1 = floor(b1) * 2.0 + 1.0;
+ vec4 sh = -step(h, vec4(0.0));
+
+ vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
+ vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
+
+ vec3 p0 = vec3(a0.xy, h.x);
+ vec3 p1 = vec3(a0.zw, h.y);
+ vec3 p2 = vec3(a1.xy, h.z);
+ vec3 p3 = vec3(a1.zw, h.w);
+
+ //Normalise gradients
+ vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
+ p0 *= norm.x;
+ p1 *= norm.y;
+ p2 *= norm.z;
+ p3 *= norm.w;
+
+ // Mix final noise value
+ vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
+ m = m * m;
+ return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
+}
+
+vec4 main( vec2 fragCoord ) {
+ float mr = min(uResolution.x, uResolution.y);
+ vec2 uv = (fragCoord * $scale - uResolution.xy) / mr;
+
+ vec2 base = uv / 2;
+
+ vec3 vColor = uColor[MAX_COLORS - 1];
+
+ const vec2 frequency = vec2(0.7, 0.3);
+ const float noiseFloor = 0.00001;
+ float t = uTime * 0.005;
+
+ for(int i = 0; i < MAX_COLORS - 1; i++) {
+ float fi = float(i);
+ float flow = 5. + fi * 0.3;
+ float speed = 6. * $speed + fi * 0.3;
+ float seed = 1. + fi * 4.;
+ float noiseCeil = 0.6 + fi * 0.07;
+
+ float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed)));
+
+ vColor = mix(vColor, uColor[i], noise);
+ }
+
+ vColor = max(vColor, uAmbient);
+
+ // Elliptical falloff centred at the very top of the screen.
+ // Using fragCoord directly (pixels) and uResolution for screen size.
+ // Horizontal radius ~ 80 % of screen width → wide enough to cover corners.
+ // Vertical radius ~ 45 % of screen height → controls how far down the glow reaches.
+ vec2 topCenter = vec2(uResolution.x * 0.5, 0.0);
+ vec2 delta = fragCoord - topCenter;
+ vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65);
+ float normDist = length(delta / radii);
+ float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5);
+
+ // Pre-multiplied alpha so the shader composites correctly over the dark background.
+ return vec4(vColor * alpha, alpha);
+}
+ """
+
+ /** Updates the animated colors in-place without recreating the shader. */
+ fun updateColors(colors: Array) {
+ colors.forEachIndexed { i, color ->
+ colorUniforms[i * 3 + 0] = color.red
+ colorUniforms[i * 3 + 1] = color.green
+ colorUniforms[i * 3 + 2] = color.blue
+ }
+ recomputeAmbient()
+ }
+
+ private fun recomputeAmbient() {
+ val count = colorCount - 1
+ var r = 0f
+ var g = 0f
+ var b = 0f
+ for (i in 0 until count) {
+ r += colorUniforms[i * 3]
+ g += colorUniforms[i * 3 + 1]
+ b += colorUniforms[i * 3 + 2]
+ }
+ val scale = 0.5f / count
+ ambientUniform[0] = r * scale
+ ambientUniform[1] = g * scale
+ ambientUniform[2] = b * scale
+ }
+
+ override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
+ super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height)
+
+ runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms)
+ runtimeEffect.setFloatUniform(
+ name = "uAmbient",
+ value1 = ambientUniform[0],
+ value2 = ambientUniform[1],
+ value3 = ambientUniform[2],
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt
new file mode 100644
index 0000000000..388914a11b
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt
@@ -0,0 +1,16 @@
+package com.tangem.core.ui.shader
+
+import com.tangem.core.ui.shader.runtime.RuntimeEffect
+
+interface TangemShader {
+ val speedModifier: Float
+ get() = 0.5f
+
+ val sksl: String
+
+ /** Applies the uniforms required for this shader to the effect */
+ fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
+ runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height)
+ runtimeEffect.setFloatUniform(name = "uTime", value1 = time)
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt
new file mode 100644
index 0000000000..899e3c8a78
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt
@@ -0,0 +1,13 @@
+package com.tangem.core.ui.shader.runtime
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+
+internal class FallbackRuntimeEffect : RuntimeEffect {
+ override val isSupported: Boolean = false
+ override val isReady: Boolean = false
+
+ override fun build(): Brush {
+ return Brush.horizontalGradient(listOf(Color.White, Color.White))
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt
new file mode 100644
index 0000000000..035495aad8
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt
@@ -0,0 +1,35 @@
+package com.tangem.core.ui.shader.runtime
+
+import android.os.Build
+import androidx.compose.ui.graphics.Brush
+import com.tangem.core.ui.shader.TangemShader
+
+interface RuntimeEffect {
+
+ val isSupported: Boolean
+ val isReady: Boolean
+
+ /** Sets a float array uniform for this shader */
+ fun setFloatUniform(name: String, value1: Float) {}
+
+ /** Sets a float array uniform for this shader */
+ fun setFloatUniform(name: String, value1: Float, value2: Float) {}
+
+ /** Sets a float array uniform for this shader */
+ fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {}
+
+ /** Sets a float array uniform for this shader */
+ fun setFloatUniform(name: String, values: FloatArray) {}
+
+ fun update(shader: TangemShader, time: Float, width: Float, height: Float) {}
+
+ fun build(): Brush
+}
+
+internal fun buildEffect(shader: TangemShader): RuntimeEffect {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ RuntimeShaderEffect(shader)
+ } else {
+ FallbackRuntimeEffect()
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt
new file mode 100644
index 0000000000..78c50e2e7c
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt
@@ -0,0 +1,41 @@
+package com.tangem.core.ui.shader.runtime
+
+import android.graphics.RuntimeShader
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.ShaderBrush
+import com.tangem.core.ui.shader.TangemShader
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect {
+ private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl)
+
+ override val isSupported: Boolean = true
+ override var isReady: Boolean = false
+
+ override fun setFloatUniform(name: String, value1: Float) {
+ compositeRuntimeEffect.setFloatUniform(name, value1)
+ }
+
+ override fun setFloatUniform(name: String, value1: Float, value2: Float) {
+ compositeRuntimeEffect.setFloatUniform(name, value1, value2)
+ }
+
+ override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {
+ compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3)
+ }
+
+ override fun setFloatUniform(name: String, values: FloatArray) {
+ compositeRuntimeEffect.setFloatUniform(name, values)
+ }
+
+ override fun update(shader: TangemShader, time: Float, width: Float, height: Float) {
+ shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height)
+ isReady = width > 0 && height > 0
+ }
+
+ override fun build(): Brush {
+ return ShaderBrush(compositeRuntimeEffect)
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt
new file mode 100644
index 0000000000..d9b8583dcb
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt
@@ -0,0 +1,6 @@
+package com.tangem.core.ui.test
+
+object AppBarWithSearchTestTags {
+ const val SEARCH_ICON = "APP_BAR_WITH_SEARCH_SEARCH_ICON"
+ const val TEXT_FIELD = "APP_BAR_WITH_SEARCH_TEXT_FIELD"
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
index 3ddbefea69..9cdf82f3f3 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
@@ -16,5 +16,7 @@ object SwapTokenScreenTestTags {
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON"
const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT"
+ const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT"
+ const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON"
const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT"
}
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp
new file mode 100644
index 0000000000..8844ed902d
Binary files /dev/null and b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp differ
diff --git a/core/ui/src/main/res/drawable/ic_arrow_back_28.xml b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml
new file mode 100644
index 0000000000..3cfbdc0aa5
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml b/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml
new file mode 100644
index 0000000000..3a1012ac04
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_arrow_collapse_24.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_calendar_20.xml b/core/ui/src/main/res/drawable/ic_calendar_20.xml
new file mode 100644
index 0000000000..46a4606330
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_calendar_20.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml
new file mode 100644
index 0000000000..6ee9686437
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml b/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml
new file mode 100644
index 0000000000..10e97276fb
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_exchange_mini_24.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_like_20.xml b/core/ui/src/main/res/drawable/ic_like_20.xml
new file mode 100644
index 0000000000..b44b0f8818
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_like_20.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_share_new_24.xml b/core/ui/src/main/res/drawable/ic_share_new_24.xml
new file mode 100644
index 0000000000..d24e87b696
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_share_new_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_shield_24.xml b/core/ui/src/main/res/drawable/ic_shield_24.xml
new file mode 100644
index 0000000000..129f023c2e
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_shield_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml
new file mode 100644
index 0000000000..69f15ed1a6
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp
new file mode 100644
index 0000000000..9d18888c54
Binary files /dev/null and b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp differ
diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp
index 2a808bbb73..0622453761 100644
Binary files a/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp and b/core/ui/src/main/res/drawable/img_tangem_pay_visa.webp differ
diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp
index bf2b79a46f..1eb05e81c8 100644
Binary files a/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp and b/core/ui/src/main/res/drawable/img_tangem_pay_visa_frozen.webp differ
diff --git a/core/ui/src/main/res/drawable/shape_triangular.xml b/core/ui/src/main/res/drawable/shape_triangular.xml
new file mode 100644
index 0000000000..c4baf11fb9
--- /dev/null
+++ b/core/ui/src/main/res/drawable/shape_triangular.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt
index fc62ab079a..5af224e026 100644
--- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt
+++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt
@@ -4,9 +4,7 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
-import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.account.converter.AccountConverterFactoryContainer
-import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
import com.tangem.data.account.repository.AccountsExpandedDTO
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
@@ -25,7 +23,6 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.account.tokens.MainAccountTokensMigration
@@ -43,12 +40,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object AccountDataModule {
- @Provides
- @Singleton
- fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles {
- return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager)
- }
-
@Provides
@Singleton
fun provideAccountsCRUDRepository(
diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt
index e283366b5b..deb207e821 100644
--- a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt
+++ b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt
@@ -1,8 +1,6 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.AccountListCryptoCurrenciesProducer
-import com.tangem.data.account.producer.DefaultMultiWalletCryptoCurrenciesProducer
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import dagger.Module
import dagger.Provides
@@ -17,10 +15,8 @@ internal object MultiWalletCryptoCurrenciesProducerModule {
@Singleton
@Provides
fun provideMultiWalletCryptoCurrenciesProducerFactory(
- accountsFeatureToggles: AccountsFeatureToggles,
- defaultImpl: DefaultMultiWalletCryptoCurrenciesProducer.Factory,
accountsImpl: AccountListCryptoCurrenciesProducer.Factory,
): MultiWalletCryptoCurrenciesProducer.Factory {
- return if (accountsFeatureToggles.isFeatureEnabled) accountsImpl else defaultImpl
+ return accountsImpl
}
}
\ No newline at end of file
diff --git a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt b/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt
deleted file mode 100644
index 0a6accf03f..0000000000
--- a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.tangem.data.account.featuretoggle
-
-import com.tangem.core.configtoggle.feature.FeatureTogglesManager
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
-
-internal class DefaultAccountsFeatureToggles(
- private val featureTogglesManager: FeatureTogglesManager,
-) : AccountsFeatureToggles {
-
- override val isFeatureEnabled: Boolean
- get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED")
-}
\ No newline at end of file
diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt
index b75e956b6e..f7901863e9 100644
--- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt
+++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt
@@ -10,6 +10,7 @@ import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
+import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER
@@ -56,6 +57,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
private val mainAccountTokensMigration: DefaultMainAccountTokensMigration,
) : WalletAccountsFetcher, WalletAccountsSaver {
+ private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
+
override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse {
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
val fetchResult = fetchWalletAccounts(userWalletId, savedAccountsResponse)
@@ -90,7 +93,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
- store.updateData { response }
+ store.updateData { response.applyTokensCompatibility() }
}
override suspend fun update(
@@ -99,7 +102,9 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
- store.updateData { transform(it) }
+ store.updateData {
+ transform(it).applyTokensCompatibility()
+ }
}
override suspend fun push(
@@ -272,6 +277,22 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
)
}
+ private fun GetWalletAccountsResponse?.applyTokensCompatibility(): GetWalletAccountsResponse? {
+ if (this == null) return null
+
+ return copy(
+ accounts = accounts.map { accountDTO ->
+ val tokens = accountDTO.tokens
+
+ if (tokens.isNullOrEmpty()) return@map accountDTO
+
+ accountDTO.copy(
+ tokens = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(tokens),
+ )
+ },
+ )
+ }
+
private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore {
return accountsResponseStoreFactory.create(userWalletId = userWalletId)
}
diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt
index 795c6dbc37..3820a2e528 100644
--- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt
+++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt
@@ -2,12 +2,11 @@ package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
-import com.tangem.data.account.store.AccountsResponseStoreFactory
-import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
+import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.flow.FlowProducerTools
-import com.tangem.domain.models.account.DerivationIndex
+import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
@@ -15,31 +14,29 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
/**
* Implementation of [MultiWalletCryptoCurrenciesProducer] that produces crypto currencies of all accounts
*
- * @property params params
- * @property userWalletsListRepository repository for getting user wallets
- * @property accountsResponseStoreFactory factory to create store with accounts response
- * @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse`
- * @property dispatchers dispatchers
+ * @property params params
+ * @property userWalletsListRepository repository for getting user wallets
+ * @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
@Assisted val params: MultiWalletCryptoCurrenciesProducer.Params,
+ private val singleAccountListSupplier: SingleAccountListSupplier,
private val userWalletsListRepository: UserWalletsListRepository,
- private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
- private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
override val flowProducerTools: FlowProducerTools,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesProducer {
override val fallback: Option> = emptySet().some()
- @Suppress("NullableToStringCall")
override fun produce(): Flow> {
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
@@ -47,23 +44,12 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet")
}
- return accountsResponseStoreFactory.create(userWalletId = userWallet.walletId).data
- .distinctUntilChanged()
- .map { response ->
- if (response == null) return@map emptySet()
-
- response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
- val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
- ?: return@map emptySet()
-
- responseCryptoCurrenciesFactory.createCurrencies(
- tokens = accountDTO.tokens.orEmpty(),
- userWallet = userWallet,
- accountIndex = accountIndex,
- )
- }
+ return singleAccountListSupplier.invoke(params.userWalletId)
+ .map { accountList ->
+ accountList.accounts
+ .filterIsInstance()
+ .flatMapTo(hashSetOf(), Account.CryptoPortfolio::cryptoCurrencies)
}
- .onEmpty { emit(emptySet()) }
.flowOn(dispatchers.default)
}
diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt
index a55c9c0da8..8dfe637ba5 100644
--- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt
+++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt
@@ -5,8 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
-import com.tangem.datasource.local.token.UserTokensResponseStore
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
@@ -23,17 +21,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency
*
* @property demoConfig demo config
* @property excludedBlockchains excluded blockchains
- * @property userWalletsStore user wallets store
- * @property userTokensResponseStore user tokens response store
*/
@Suppress("LongParameterList")
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsListRepository: UserWalletsListRepository,
- private val accountsFeatureToggles: AccountsFeatureToggles,
private val walletAccountsFetcher: WalletAccountsFetcher,
- private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
) : CardCryptoCurrencyFactory {
@@ -140,34 +134,21 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set,
): Map> {
- val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) {
- val response = walletAccountsFetcher.getSaved(userWallet.walletId)
- ?: return emptyMap()
+ val response = walletAccountsFetcher.getSaved(userWallet.walletId)
+ ?: return emptyMap()
- response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
- val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
- ?: return@flatMapTo emptySet()
-
- responseCryptoCurrenciesFactory.createCurrencies(
- tokens = accountDTO.tokens.orEmpty().filter { token ->
- networks.any {
- it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
- }
- },
- userWallet = userWallet,
- accountIndex = accountIndex,
- )
- }
- } else {
- val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
- ?: return emptyMap()
+ val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
+ val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
+ ?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
- tokens = response.tokens.filter { token ->
- networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
+ tokens = accountDTO.tokens.orEmpty().filter { token ->
+ networks.any {
+ it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
+ }
},
userWallet = userWallet,
- accountIndex = DerivationIndex.Main,
+ accountIndex = accountIndex,
)
}
.groupBy(CryptoCurrency::network)
@@ -181,28 +162,17 @@ internal class DefaultCardCryptoCurrencyFactory(
): Map> {
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
- return if (accountsFeatureToggles.isFeatureEnabled) {
- val response = walletAccountsFetcher.getSaved(userWallet.walletId)
- ?: return emptyMap()
+ val response = walletAccountsFetcher.getSaved(userWallet.walletId)
+ ?: return emptyMap()
- response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
- val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
- ?: return@flatMapTo emptySet()
-
- responseCryptoCurrenciesFactory.createCurrencies(
- tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
- userWallet = userWallet,
- accountIndex = accountIndex,
- )
- }
- } else {
- val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
- ?: return emptyMap()
+ return response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
+ val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
+ ?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
- tokens = response.tokens.filter { token -> token.networkId in networkIds },
+ tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
userWallet = userWallet,
- accountIndex = DerivationIndex.Main,
+ accountIndex = accountIndex,
)
}
.groupBy { it.network.id.rawId }
diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt
index cff0ae1d6d..a1ab6f180b 100644
--- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt
+++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt
@@ -8,10 +8,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
-import com.tangem.datasource.api.tangemTech.models.WalletType
-import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.token.UserTokensResponseStore
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncOrNull
import com.tangem.domain.models.wallet.UserWallet
@@ -30,8 +27,6 @@ class UserTokensSaver(
private val dispatchers: CoroutineDispatcherProvider,
private val addressesEnricher: UserTokensResponseAddressesEnricher,
private val walletServerBinder: WalletServerBinder,
- private val appsFlyerStore: AppsFlyerStore,
- private val accountsFeatureToggles: AccountsFeatureToggles,
private val pushTokensRetryerPool: RetryerPool,
) {
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
@@ -68,22 +63,9 @@ class UserTokensSaver(
return@withContext
}
- if (accountsFeatureToggles.isFeatureEnabled) {
- val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
+ val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
- pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
- } else {
- val conversionData = appsFlyerStore.get()
-
- val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
- walletName = userWallet.name.takeIf { it.isNotBlank() },
- walletType = WalletType.from(userWallet),
- refcode = conversionData?.refcode,
- campaign = conversionData?.campaign,
- )
-
- pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)
- }
+ push(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
}
suspend fun pushWithRetryer(
@@ -103,14 +85,7 @@ class UserTokensSaver(
)
}
- private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) {
- safeApiCall(
- call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() },
- onError = { onFailSend() },
- )
- }
-
- private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
+ private suspend fun push(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
safeApiCall(
call = {
val apiResponse = tangemTechApi.saveTokens(
@@ -148,13 +123,7 @@ class UserTokensSaver(
return this
.enrichByAddress(userWalletId = userWalletId)
- .let { response ->
- if (accountsFeatureToggles.isFeatureEnabled) {
- response.enrichByAccountId(userWalletId = userWalletId)
- } else {
- response
- }
- }
+ .enrichByAccountId(userWalletId = userWalletId)
}
private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse {
diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt
index 9ce3a6b121..2d0f7de535 100644
--- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt
+++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt
@@ -13,7 +13,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.walletmanager.WalletManagersFacade
@@ -37,18 +36,14 @@ internal object DataCommonModule {
fun provideCardCryptoCurrencyFactory(
excludedBlockchains: ExcludedBlockchains,
userWalletsListRepository: UserWalletsListRepository,
- accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
- userTokensResponseStore: UserTokensResponseStore,
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
): CardCryptoCurrencyFactory {
return DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig,
excludedBlockchains = excludedBlockchains,
userWalletsListRepository = userWalletsListRepository,
- accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
- userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
)
}
@@ -76,8 +71,6 @@ internal object DataCommonModule {
dispatchers: CoroutineDispatcherProvider,
addressesEnricher: UserTokensResponseAddressesEnricher,
walletServerBinder: WalletServerBinder,
- appsFlyerStore: AppsFlyerStore,
- accountsFeatureToggles: AccountsFeatureToggles,
): UserTokensSaver {
return UserTokensSaver(
tangemTechApi = tangemTechApi,
@@ -85,12 +78,10 @@ internal object DataCommonModule {
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
addressesEnricher = addressesEnricher,
- accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = RetryerPool(
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
),
walletServerBinder = walletServerBinder,
- appsFlyerStore = appsFlyerStore,
)
}
diff --git a/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt b/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt
index 54e69b23a1..f9c12f4d03 100644
--- a/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt
+++ b/data/common/src/main/kotlin/com/tangem/data/common/tokens/UserTokensBackwardCompatibility.kt
@@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
* Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens
* in new application with new IDs
*/
-internal class UserTokensBackwardCompatibility {
+class UserTokensBackwardCompatibility {
fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse {
return userTokensResponse.copy(
@@ -28,6 +28,21 @@ internal class UserTokensBackwardCompatibility {
)
}
+ fun applyCompatibilityAndGetUpdated(tokens: List): List {
+ return tokens.map { token ->
+ val oldSavedId = NETWORKS_TO_OLD_SAVED_IDS[token.networkId]
+ if (oldSavedId != null && token.id == oldSavedId) {
+ Blockchain.fromNetworkId(token.networkId)?.let { blockchain ->
+ token.copy(
+ id = blockchain.toCoinId(),
+ )
+ } ?: token
+ } else {
+ token
+ }
+ }
+ }
+
companion object {
private val NETWORKS_TO_OLD_SAVED_IDS = mapOf(
"optimistic-ethereum" to "ethereum",
diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt
index eb90a1efb0..749eadff8b 100644
--- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt
+++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt
@@ -11,8 +11,8 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
-import com.tangem.datasource.local.token.UserTokensResponseStore
-import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
+import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
+import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
@@ -38,20 +38,16 @@ import org.junit.jupiter.params.ParameterizedTest
internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk()
- private val userTokensResponseStore: UserTokensResponseStore = mockk()
private val excludedBlockchains = ExcludedBlockchains()
- private val accountsFeatureToggles = mockk()
private val walletAccountsFetcher = mockk()
private val factory = DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig,
excludedBlockchains = excludedBlockchains,
userWalletsListRepository = userWalletsListRepository,
- userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(
networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains),
),
- accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
@@ -64,7 +60,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
@BeforeEach
fun init() {
- clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri)
+ clearMocks(userWalletsListRepository, walletAccountsFetcher, iconUri)
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns iconUri
@@ -80,12 +76,11 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Arrange
val userWallet = createMultiWallet()
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
- val userTokensResponse = model.userTokensResponse
+ val accountsResponse = model.accountsResponse
val network = ethereum.network
- every { accountsFeatureToggles.isFeatureEnabled } returns false
every { userWalletsListRepository.userWallets } returns userWalletsFlow
- coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
+ coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse
// Act
val actual = factory.create(userWalletId = userWallet.walletId, network = network)
@@ -97,25 +92,25 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWallets
- userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
+ walletAccountsFetcher.getSaved(userWalletId = userWallet.walletId)
}
}
private fun provideTestModels() = listOf(
- CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()),
- CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()),
+ CreateTestModel.MultiWallet(accountsResponse = null, expected = emptyList()),
+ CreateTestModel.MultiWallet(accountsResponse = createAccountsResponse(), expected = emptyList()),
CreateTestModel.MultiWallet(
- userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)),
+ accountsResponse = createAccountsResponse(currencies = listOf(ethereum)),
expected = listOf(ethereum),
),
CreateTestModel.MultiWallet(
- userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)),
+ accountsResponse = createAccountsResponse(listOf(element = bitcoin)),
expected = emptyList(),
),
)
- private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse {
- return userTokensResponseFactory.createUserTokensResponse(
+ private fun createAccountsResponse(currencies: List = emptyList()): GetWalletAccountsResponse {
+ return createWalletAccountsResponse(
currencies = currencies,
isGroupedByNetwork = false,
isSortedByBalance = false,
@@ -150,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
}
coVerify(inverse = true) {
- userTokensResponseStore.getSyncOrNull(userWalletId = any())
+ walletAccountsFetcher.getSaved(userWalletId = any())
}
}
@@ -195,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
}
coVerify(inverse = true) {
- userTokensResponseStore.getSyncOrNull(userWalletId = any())
+ walletAccountsFetcher.getSaved(userWalletId = any())
}
}
@@ -218,7 +213,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
val expected: List
data class MultiWallet(
- val userTokensResponse: UserTokensResponse?,
+ val accountsResponse: GetWalletAccountsResponse?,
override val expected: List,
) : CreateTestModel
@@ -245,10 +240,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Arrange
val userWallet = model.multiWallet
val networks = setOf(ethereum.network, bitcoin.network)
- val userTokensResponse = model.userTokensResponse
+ val accountsResponse = model.accountsResponse
- every { accountsFeatureToggles.isFeatureEnabled } returns false
- coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
+ coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse
// Act
val actual = runCatching {
@@ -273,19 +267,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
private fun provideTestModels() = listOf(
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
- userTokensResponse = null,
+ accountsResponse = null,
expected = Result.success(emptyMap()),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
- userTokensResponse = createUserTokensResponse(),
+ accountsResponse = createWalletAccountsResponse(emptyList(), false, false),
expected = Result.success(
setOf(ethereum.network, bitcoin.network).associateWith { emptyList() },
),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
- userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)),
+ accountsResponse = createWalletAccountsResponse(currencies = listOf(bitcoin, ethereum), false, false),
expected = mapOf(
bitcoin.network to listOf(bitcoin),
ethereum.network to listOf(ethereum),
@@ -293,12 +287,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createSingleWallet(),
- userTokensResponse = null,
+ accountsResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = MockUserWalletFactory.createSingleWalletWithToken(),
- userTokensResponse = null,
+ accountsResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
)
@@ -306,7 +300,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
data class CreateCurrenciesForMultiWalletModel(
val multiWallet: UserWallet,
- val userTokensResponse: UserTokensResponse?,
+ val accountsResponse: GetWalletAccountsResponse?,
val expected: Result