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/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index cf540d0ec0..ea612e1a2d 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
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
@@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
- @Inject
- internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
-
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow
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..714a4acfd7 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,19 @@ 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(applicationContext, message)
+ }
+
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..3be28c02e2 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,12 @@ 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 +56,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(
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/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/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/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/strings.xml b/core/res/src/main/res/values/strings.xml
index f40c33cb35..b03f0df753 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -851,6 +851,7 @@
Related tokens
Related news
Stay in the loop
+ Trending score
NFC is not available on your device
About NFT
NFT asset
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/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