Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 13:02:12 +03:00
commit 3946e4ebca
359 changed files with 8459 additions and 3324 deletions

View file

@ -273,6 +273,8 @@ dependencies {
implementation(projects.features.welcome.impl) implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.api)
implementation(projects.features.createWalletSelection.impl) implementation(projects.features.createWalletSelection.impl)
implementation(projects.features.createWalletStart.api)
implementation(projects.features.createWalletStart.impl)
implementation(projects.features.home.api) implementation(projects.features.home.api)
implementation(projects.features.home.impl) implementation(projects.features.home.impl)
implementation(projects.features.account.api) implementation(projects.features.account.api)

View file

@ -1,8 +1,8 @@
package com.tangem.common package com.tangem.common
import android.Manifest import android.Manifest
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
@ -121,6 +121,8 @@ abstract class BaseTestCase : TestCase(
/** /**
* Prints the Compose semantics tree to logcat for debugging UI tests. * Prints the Compose semantics tree to logcat for debugging UI tests.
* *
* @param rootIndex Use rootIndex > 0, if you need to print semantics tree for bottom sheet.
* Default: 0.
* @param useUnmergedTree When true, shows unmerged tree with all individual nodes. * @param useUnmergedTree When true, shows unmerged tree with all individual nodes.
* Use for accessing inner elements of compound components. * Use for accessing inner elements of compound components.
* Default: false (merged tree - accessibility view). * Default: false (merged tree - accessibility view).
@ -129,11 +131,13 @@ abstract class BaseTestCase : TestCase(
* Default: Int.MAX_VALUE (unlimited depth). * Default: Int.MAX_VALUE (unlimited depth).
*/ */
fun printSemanticTree( fun printSemanticTree(
rootIndex: Int = 0,
useUnmergedTree: Boolean = false, useUnmergedTree: Boolean = false,
tag: String = "SEMANTIC_TREE", tag: String = "SEMANTIC_TREE",
maxDepth: Int = Int.MAX_VALUE) maxDepth: Int = Int.MAX_VALUE
{ ) {
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth) composeTestRule.onAllNodes(isRoot(), useUnmergedTree = useUnmergedTree)[rootIndex]
.printToLog(tag, maxDepth)
} }
fun waitForIdle() = composeTestRule.waitForIdle() fun waitForIdle() = composeTestRule.waitForIdle()

View file

@ -4,6 +4,7 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18" const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0" const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p" const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val WAIT_UNTIL_TIMEOUT = 20_000L const val WAIT_UNTIL_TIMEOUT = 20_000L

View file

@ -0,0 +1,38 @@
package com.tangem.common.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.test.core.app.ApplicationProvider
fun getClipboardText(context: Context): String? {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
return if (clipboard.hasPrimaryClip()) {
clipboard.primaryClip?.getItemAt(0)?.text?.toString()
} else {
null
}
}
fun setClipboardText(context: Context, text: String?) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("label", text)
clipboard.setPrimaryClip(clip)
}
fun clearClipboard(
context: Context = ApplicationProvider.getApplicationContext()
) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.clearPrimaryClip()
}
fun assertClipboardTextEquals(
expected: String,
context: Context = ApplicationProvider.getApplicationContext()
) {
val actual = getClipboardText(context)
assert(actual == expected) {
"Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'"
}
}

View file

@ -85,4 +85,13 @@ fun BaseTestCase.openDeviceSettingsScreen() {
step("Click on 'Device settings' button") { step("Click on 'Device settings' button") {
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() } onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
} }
}
fun BaseTestCase.openWalletConnectScreen() {
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
} }

View file

@ -0,0 +1,124 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onSendScreen
import com.tangem.screens.onStakingConfirmScreen
import com.tangem.screens.onStakingDetailsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkStakingDetailsScreen(withStaking: Boolean) {
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
if (withStaking) {
step("Assert 'Rewards block' is displayed") {
onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' title is displayed") {
onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() }
}
step("Assert 'Rewards block' text is displayed") {
onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() }
}
step("Assert 'Active staking block' is displayed") {
onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() }
}
step("Assert 'Your stakes' title is displayed") {
onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() }
}
step("Assert 'Stake more' button is displayed") {
onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() }
}
} else {
step("Assert banner image is displayed") {
onStakingDetailsScreen { bannerImage.assertIsDisplayed() }
}
step("Assert banner text is displayed") {
onStakingDetailsScreen { bannerText.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingDetailsScreen { stakeButton.assertIsDisplayed() }
}
}
}
fun BaseTestCase.checkStakingScreen(stakingAmount: String) {
step("Assert 'Staking' screen is displayed") {
onSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert top app bar 'Close' button is displayed") {
onSendScreen { closeButton.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert token name is displayed") {
onSendScreen { tokenName.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onSendScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert 'Max' button is displayed") {
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onSendScreen { nextButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkStakingConfirmScreen() {
step("Assert 'Staking confirm' screen title is displayed") {
onStakingConfirmScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.screens.onWalletConnectScreen
import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkWalletConnectBottomSheet() { fun BaseTestCase.checkWalletConnectBottomSheet() {
waitForIdle()
step("Assert 'Wallet Connect' bottom sheet title is displayed") { step("Assert 'Wallet Connect' bottom sheet title is displayed") {
onWalletConnectBottomSheet { title.assertIsDisplayed() } onWalletConnectBottomSheet { title.assertIsDisplayed() }
} }
@ -60,34 +61,64 @@ fun BaseTestCase.checkWalletConnectBottomSheet() {
} }
} }
fun BaseTestCase.checkWalletConnectScreen() { fun BaseTestCase.checkWalletConnectScreen(withConnections: Boolean) {
waitForIdle()
step("Assert 'Wallet Connect' title is displayed") { step("Assert 'Wallet Connect' title is displayed") {
onWalletConnectScreen { title.assertIsDisplayed() } onWalletConnectScreen { title.assertIsDisplayed() }
} }
step("Assert 'More' button is displayed") {
onWalletConnectScreen { moreButton.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectScreen { walletName.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectScreen { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectScreen { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectScreen { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectScreen { appUrl.assertIsDisplayed() }
}
step("Assert 'New Connection' button is displayed") { step("Assert 'New Connection' button is displayed") {
onWalletConnectScreen { newConnectionButton.assertIsDisplayed() } onWalletConnectScreen { newConnectionButton.assertIsDisplayed() }
} }
if (withConnections) {
step("Assert 'More' button is displayed") {
onWalletConnectScreen { moreButton.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectScreen { walletName.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectScreen { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectScreen { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectScreen { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectScreen { appUrl.assertIsDisplayed() }
}
} else {
step("Assert wallet name is not displayed") {
onWalletConnectScreen { walletName.assertIsNotDisplayed() }
}
step("Assert app icon is not displayed") {
onWalletConnectScreen { appIcon.assertIsNotDisplayed() }
}
step("Assert app name is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
step("Assert approve icon is not displayed") {
onWalletConnectScreen { approveIcon.assertIsNotDisplayed() }
}
step("Assert app URL is not displayed") {
onWalletConnectScreen { appUrl.assertIsNotDisplayed() }
}
step("Assert 'Wallet Connect' image is displayed") {
onWalletConnectScreen { walletConnectImage.assertIsDisplayed() }
}
step("Assert 'No session' title is displayed") {
onWalletConnectScreen { noSessionTitle.assertIsDisplayed() }
}
step("Assert 'No session' text is displayed") {
onWalletConnectScreen { noSessionText.assertIsDisplayed() }
}
}
} }
fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
waitForIdle()
step("Assert connection details title is displayed") { step("Assert connection details title is displayed") {
onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() } onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() }
} }
@ -128,7 +159,7 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() } onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() }
} }
step("Assert connected dApp name: '$dAppName'") { step("Assert connected dApp name: '$dAppName'") {
onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) } onWalletConnectDetailsBottomSheet { appName.assertTextContains(dAppName) }
} }
step("Assert connected network symbol is displayed") { step("Assert connected network symbol is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() } onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() }

View file

@ -0,0 +1,55 @@
package com.tangem.screens
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import com.kaspersky.kaspresso.screens.KScreen
object ChromeBrowserPageObject : KScreen<ChromeBrowserPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private const val TIMEOUT = 10000L
fun assertChromeIsOpened() {
val chromePackage = "com.android.chrome"
device.wait(Until.hasObject(By.pkg(chromePackage).depth(0)), TIMEOUT)
assert(device.hasObject(By.pkg(chromePackage))) {
"Chrome browser is not opened"
}
}
fun assertUrlContains(expectedUrl: String) {
val urlBar = device.findObject(
By.res("com.android.chrome:id/url_bar")
)
urlBar?.let {
assert(it.text.contains(expectedUrl)) {
"URL doesn't contain expected: $expectedUrl, actual: ${it.text}"
}
}
}
private fun findElementByText(text: String): UiObject2? {
device.wait(Until.hasObject(By.text(text)), TIMEOUT)
return device.findObject(By.text(text))
}
fun assertElementWithTextExists(text: String) {
val element = findElementByText(text)
assert(element != null) { "Element with text '$text' not found" }
}
fun isElementWithTextExists(text: String): Boolean {
return device.wait(Until.hasObject(By.text(text)), TIMEOUT)
}
fun clickOnElementWithText(text: String) {
findElementByText(text)?.click()
?: throw AssertionError("Cannot click - element with text '$text' not found")
}
}

View file

@ -17,6 +17,7 @@ import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText import androidx.compose.ui.test.hasText as withText
import com.tangem.core.ui.R as CoreUiR
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
@ -200,6 +201,10 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true useUnmergedTree = true
} }
val snackbarCopiedAddressMessage: KNode = child {
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
}
/** /**
* Find token list item with title and address * Find token list item with title and address
*/ */

View file

@ -3,20 +3,19 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
class BaseBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BaseBottomSheetPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val hideButton: KNode = child { val showQrCodeButton: KNode = child {
hasText(getResourceString(R.string.token_details_hide_token)) hasText(getResourceString(R.string.token_receive_show_qr_code_title))
hasTestTag(BaseBottomSheetTestTags.ACTION_TITLE) useUnmergedTree = true
} }
} }
internal fun BaseTestCase.onBottomSheet(function: BaseBottomSheetPageObject.() -> Unit) = internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function) onComposeScreen(composeTestRule, function)

View file

@ -10,7 +10,6 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasTestTag as withTestTag
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
@ -19,6 +18,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(SendScreenTestTags.SCREEN_CONTAINER) hasTestTag(SendScreenTestTags.SCREEN_CONTAINER)
} }
val closeButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val title: KNode = child { val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE) hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true useUnmergedTree = true
@ -29,13 +33,18 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true useUnmergedTree = true
} }
val amountContainerText: KNode = child { val amountInputTextField: KNode = child {
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT) hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true useUnmergedTree = true
} }
val amountInputTextField: KNode = child { val tokenName: KNode = child {
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD) hasTestTag(SendScreenTestTags.TOKEN_NAME)
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(SendScreenTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true useUnmergedTree = true
} }
@ -44,28 +53,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true useUnmergedTree = true
} }
val currencyButton: KNode = child {
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.CURRENCY_ICON))
useUnmergedTree = true
}
val fiatButton: KNode = child {
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.FIAT_ICON))
useUnmergedTree = true
}
val maxButton: KNode = child { val maxButton: KNode = child {
hasTestTag(SendScreenTestTags.MAX_BUTTON) hasTestTag(SendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true useUnmergedTree = true
} }
val previousButton: KNode = child {
hasTestTag(SendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child { val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_next)) hasText(getResourceString(SendR.string.common_next))

View file

@ -71,6 +71,10 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(R.string.common_swap)) hasText(getResourceString(R.string.common_swap))
} }
fun tokenSymbol(symbol: String): KNode = child {
hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL)
}
} }
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) = internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =

View file

@ -0,0 +1,75 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class TokenActionsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenActionsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val analyticsButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_analytics)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val copyAddressButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_copy_address)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val receiveButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_receive)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val sendButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_send)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val swapButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_swap)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val buyButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_buy)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val sellButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_sell)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val hideTokenButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.token_details_hide_token)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenActionsBottomSheet(function: TokenActionsBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,56 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class TokenReceiveQrCodeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenReceiveQrCodeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val closeButton: KNode = child {
hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val title: KNode = child {
hasTestTag(TokenReceiveQrCodeBottomSheetTestTags.TITLE)
useUnmergedTree = true
}
val qrCode: KNode = child {
hasTestTag(TokenReceiveQrCodeBottomSheetTestTags.QR_CODE)
useUnmergedTree = true
}
val addressTitle: KNode = child {
hasText(getResourceString(R.string.wc_common_address))
useUnmergedTree = true
}
val address: KNode = child {
hasTestTag(TokenReceiveQrCodeBottomSheetTestTags.ADDRESS)
useUnmergedTree = true
}
val copyButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_copy))
useUnmergedTree = true
}
val shareButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_share))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenReceiveQrCodeBottomSheet(function: TokenReceiveQrCodeBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,28 @@
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.BaseButtonTestTags
import com.tangem.core.ui.test.TokenReceiveWarningBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class TokenReceiveWarningBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenReceiveWarningBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val bottomSheet: KNode = child {
hasTestTag(TokenReceiveWarningBottomSheetTestTags.BOTTOM_SHEET)
}
val gotItButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_got_it))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenReceiveWarningBottomSheet(function: TokenReceiveWarningBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -9,13 +9,11 @@ import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child { val title: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect))
hasTestTag(WalletConnectBottomSheetTestTags.TITLE) hasTestTag(WalletConnectBottomSheetTestTags.TITLE)
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -53,6 +53,21 @@ class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
hasText(getResourceString(R.string.wc_new_connection)) hasText(getResourceString(R.string.wc_new_connection))
useUnmergedTree = true useUnmergedTree = true
} }
val walletConnectImage: KNode = child {
hasTestTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE)
useUnmergedTree = true
}
val noSessionTitle: KNode = child {
hasText(getResourceString(R.string.wc_no_sessions_title))
useUnmergedTree = true
}
val noSessionText: KNode = child {
hasText(getResourceString(R.string.wc_no_sessions_desc))
useUnmergedTree = true
}
} }
internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) = internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) =

View file

@ -0,0 +1,23 @@
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.BaseButtonTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletConnectScanQrPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectScanQrPageObject>(semanticsProvider = semanticsProvider) {
val pasteFromClipboardButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wallet_connect_paste_from_clipboard))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectScanQrScreen(function: WalletConnectScanQrPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,6 +1,10 @@
package com.tangem.tests package com.tangem.tests
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.checkMultiCurrencyMainScreen import com.tangem.scenarios.checkMultiCurrencyMainScreen
import com.tangem.scenarios.checkSingleCurrencyMainScreen import com.tangem.scenarios.checkSingleCurrencyMainScreen
@ -15,6 +19,10 @@ import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
class ScanCardTest : BaseTestCase() { class ScanCardTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD),
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("868") @AllureId("868")
@DisplayName("Scan: Scanning single-currency cards") @DisplayName("Scan: Scanning single-currency cards")
@Test @Test

View file

@ -7,9 +7,8 @@ import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.*
import com.tangem.screens.* import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -117,116 +116,20 @@ class StakingTest : BaseTestCase() {
step("Click on 'Staking block'") { step("Click on 'Staking block'") {
onTokenDetailsScreen { stakingBlock.clickWithAssertion() } onTokenDetailsScreen { stakingBlock.clickWithAssertion() }
} }
step("Assert 'Title' is displayed") { step("Check 'Staking details' screen") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } checkStakingDetailsScreen(withStaking = true)
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' is displayed") {
onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' title is displayed") {
onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() }
}
step("Assert 'Rewards block' text is displayed") {
onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() }
}
step("Assert 'Active staking block' is displayed") {
onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() }
}
step("Assert 'Your stakes' title is displayed") {
onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake more' button is displayed") {
onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() }
} }
step("Click 'Stake more' button") { step("Click 'Stake more' button") {
onStakingDetailsScreen { stakeMoreButton.performClick() } onStakingDetailsScreen { stakeMoreButton.performClick() }
} }
step("Assert 'Send' screen is displayed") { step("Check 'Staking' screen") {
onSendScreen { screenContainer.assertIsDisplayed() } checkStakingScreen(stakingAmount)
}
step("Assert 'Send' screen title is displayed") {
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onSendScreen { nextButton.assertIsDisplayed() }
} }
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendScreen { nextButton.performClick() } onSendScreen { nextButton.performClick() }
} }
step("Assert 'Send details' screen title is displayed") { step("Check 'Staking confirm' screen") {
onStakingConfirmScreen { title.assertIsDisplayed() } checkStakingConfirmScreen()
}
step("Assert primary amount is displayed") {
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
} }
} }
} }
@ -284,107 +187,20 @@ class StakingTest : BaseTestCase() {
step("Click on 'Stake' button") { step("Click on 'Stake' button") {
onTokenDetailsScreen { stakeButton.clickWithAssertion() } onTokenDetailsScreen { stakeButton.clickWithAssertion() }
} }
step("Assert 'Title' is displayed") { step("Check 'Staking details' screen") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } checkStakingDetailsScreen(withStaking = false)
}
step("Assert banner image is displayed") {
onStakingDetailsScreen { bannerImage.assertIsDisplayed() }
}
step("Assert banner text is displayed") {
onStakingDetailsScreen { bannerText.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingDetailsScreen { stakeButton.assertIsDisplayed() }
} }
step("Click 'Stake' button") { step("Click 'Stake' button") {
onStakingDetailsScreen { stakeButton.performClick() } onStakingDetailsScreen { stakeButton.performClick() }
} }
step("Assert 'Send' screen is displayed") { step("Check 'Staking' screen") {
onSendScreen { screenContainer.assertIsDisplayed() } checkStakingScreen(stakingAmount)
}
step("Assert 'Send' screen title is displayed") {
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onSendScreen { nextButton.assertIsDisplayed() }
} }
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendScreen { nextButton.performClick() } onSendScreen { nextButton.performClick() }
} }
step("Assert 'Send details' screen title is displayed") { step("Check 'Staking confirm' screen") {
onStakingConfirmScreen { title.assertIsDisplayed() } checkStakingConfirmScreen()
}
step("Assert primary amount is displayed") {
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
} }
} }
} }

View file

@ -2,12 +2,13 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.getWcUri import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.* import com.tangem.scenarios.*
import com.tangem.screens.* import com.tangem.screens.*
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -21,7 +22,7 @@ class WalletConnectTest : BaseTestCase() {
@DisplayName("WC (React App): open session from deeplink on main screen") @DisplayName("WC (React App): open session from deeplink on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test @Test
fun openWalletConnectSessionOnMainScreen() { fun openWalletConnectSessionOnMainScreenTest() {
val balance = TOTAL_BALANCE val balance = TOTAL_BALANCE
val dAppName = "React App" val dAppName = "React App"
val deepLinkUri = getWcUri() val deepLinkUri = getWcUri()
@ -37,19 +38,28 @@ class WalletConnectTest : BaseTestCase() {
openAppByDeepLink(deepLinkUri) openAppByDeepLink(deepLinkUri)
} }
step("Check 'Wallet Connect' bottom sheet") { step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet() flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Assert 'Connect' button is enabled") {
onWalletConnectBottomSheet { connectButton.assertIsEnabled() }
} }
step("Click on 'Connect' button") { step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() } onWalletConnectBottomSheet { connectButton.performClick() }
} }
step("Click 'More' button on TopBar") { step("Assert 'Connect' button is not displayed") {
onTopBar { moreButton.clickWithAssertion() } waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
} }
step("Click on 'Wallet Connect' button") { step("Open 'Wallet Connect' screen") {
onDetailsScreen { walletConnectButton.clickWithAssertion() } openWalletConnectScreen()
} }
step("Check 'Wallet Connect' screen") { step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen() flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectScreen(withConnections = true)
}
} }
step("Click on app icon") { step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() } onWalletConnectScreen { appIcon.performClick() }
@ -57,11 +67,11 @@ class WalletConnectTest : BaseTestCase() {
step("Check 'Wallet Connect' details bottom sheet") { step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName) checkWalletConnectDetailsBottomSheet(dAppName)
} }
step("Click on 'Disconnect button' is displayed") { step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
} }
step("Assert connection is not displayed") { step("Check 'Wallet Connect' screen without connections") {
onWalletConnectScreen { appName.assertIsNotDisplayed() } checkWalletConnectScreen(withConnections = false)
} }
} }
} }
@ -70,7 +80,7 @@ class WalletConnectTest : BaseTestCase() {
@DisplayName("WC (React App): open session from deeplink not on main screen") @DisplayName("WC (React App): open session from deeplink not on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test @Test
fun openWalletConnectSessionNotOnMainScreen() { fun openWalletConnectSessionNotOnMainScreenTest() {
val balance = TOTAL_BALANCE val balance = TOTAL_BALANCE
val dAppName = "React App" val dAppName = "React App"
val deepLinkUri = getWcUri() val deepLinkUri = getWcUri()
@ -82,41 +92,44 @@ class WalletConnectTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses(balance) synchronizeAddresses(balance)
} }
step("Click on 'Buy' button") { step("Open 'Wallet Connect' screen") {
onMainScreen { buyButton.clickWithAssertion() } openWalletConnectScreen()
checkWalletConnectScreen(false)
} }
step("Create WC session buy deeplink") { step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri) openAppByDeepLink(deepLinkUri)
} }
step("Check 'Wallet Connect' bottom sheet") { step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet() flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
} }
step("Click on 'Connect' button") { step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() } onWalletConnectBottomSheet { connectButton.performClick() }
} }
step("Click 'More' button on TopBar") { step("Assert 'Connect' button is not displayed") {
onTopBar { moreButton.clickWithAssertion() } waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
} }
step("Click on 'Wallet Connect' button") { step("Check 'Wallet Connect' screen with connections") {
onDetailsScreen { walletConnectButton.clickWithAssertion() } flakySafely(WAIT_UNTIL_TIMEOUT) {
} checkWalletConnectScreen(withConnections = true)
step("Assert 'Wallet Connect' bottom sheet is displayed") { }
onWalletConnectBottomSheet { connectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
} }
step("Click on app icon") { step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() } onWalletConnectScreen { appIcon.performClick() }
} }
step("Check 'Wallet Connect' details bottom sheet") { step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName) flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectDetailsBottomSheet(dAppName)
}
} }
step("Click on 'Disconnect button' is displayed") { step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
} }
step("Assert connection is not displayed") { step("Check 'Wallet Connect' screen without connections") {
onWalletConnectScreen { appName.assertIsNotDisplayed() } checkWalletConnectScreen(withConnections = false)
} }
} }
} }
@ -125,9 +138,10 @@ class WalletConnectTest : BaseTestCase() {
@DisplayName("WC (React App): open session from deeplink ") @DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test @Test
fun openWalletConnectSession() { fun openWalletConnectSessionTest() {
val balance = TOTAL_BALANCE val balance = TOTAL_BALANCE
val dAppName = "React App" val dAppName = "React App"
val packageName = BuildConfig.APPLICATION_ID
val deepLinkUri = getWcUri() val deepLinkUri = getWcUri()
setupHooks().run { setupHooks().run {
@ -137,32 +151,28 @@ class WalletConnectTest : BaseTestCase() {
step("Synchronize addresses") { step("Synchronize addresses") {
synchronizeAddresses(balance) synchronizeAddresses(balance)
} }
step("Open recent apps") { step("Kill app") {
device.uiDevice.pressRecentApps() device.apps.kill(packageName)
}
step("Stop app by swipe") {
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.8f)
} }
step("Create WC session buy deeplink") { step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri) openAppByDeepLink(deepLinkUri)
} }
step("Open 'Main Screen'") {
openMainScreen()
}
step("Check 'Wallet Connect' bottom sheet") { step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet() flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
} }
step("Click on 'Connect' button") { step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() } onWalletConnectBottomSheet { connectButton.performClick() }
} }
step("Click 'More' button on TopBar") { step("Assert 'Connect' button is not displayed") {
onTopBar { moreButton.clickWithAssertion() } onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
} }
step("Click on 'Wallet Connect' button") { step("Open 'Wallet Connect' screen") {
onDetailsScreen { walletConnectButton.clickWithAssertion() } openWalletConnectScreen()
} }
step("Check 'Wallet Connect' screen") { step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen() checkWalletConnectScreen(withConnections = true)
} }
step("Click on app icon") { step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() } onWalletConnectScreen { appIcon.performClick() }
@ -170,11 +180,72 @@ class WalletConnectTest : BaseTestCase() {
step("Check 'Wallet Connect' details bottom sheet") { step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName) checkWalletConnectDetailsBottomSheet(dAppName)
} }
step("Click on 'Disconnect button' is displayed") { step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
} }
step("Assert connection is not displayed") { step("Check 'Wallet Connect' screen without connections") {
onWalletConnectScreen { appName.assertIsNotDisplayed() } checkWalletConnectScreen(withConnections = false)
}
}
}
@AllureId("887")
@DisplayName("WC: open session by 'Paste from clipboard' button")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionByClipboardLinkTest() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val context = device.context
val deepLinkUri = getWcUri()
setupHooks().run {
step("Set URI to clipboard") {
setClipboardText(context, deepLinkUri)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Click 'New connection' button") {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("CLick 'Paste from clipboard' button") {
onWalletConnectScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
} }
} }
} }

View file

@ -0,0 +1,405 @@
package com.tangem.tests.actionButtons
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onTokenActionsBottomSheet
import com.tangem.screens.onBuyTokenDetailsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onMainScreen
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 MainScreenActionButtonsTest : BaseTestCase() {
@AllureId("79")
@DisplayName("Action buttons (long tap): validate UI")
@Test
fun actionButtonsValidateLongTapUiTest() {
val tokenTitle = "Ethereum"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Analytics' button is displayed") {
onTokenActionsBottomSheet { analyticsButton.assertIsDisplayed() }
}
step("Assert 'Copy address' button is displayed") {
onTokenActionsBottomSheet { copyAddressButton.assertIsDisplayed() }
}
step("Assert 'Receive' button is displayed") {
onTokenActionsBottomSheet { receiveButton.assertIsDisplayed() }
}
step("Assert 'Send' button is displayed") {
onTokenActionsBottomSheet { sendButton.assertIsDisplayed() }
}
step("Assert 'Swap' button is displayed") {
onTokenActionsBottomSheet { swapButton.assertIsDisplayed() }
}
step("Assert 'Buy' button is displayed") {
onTokenActionsBottomSheet { buyButton.assertIsDisplayed() }
}
step("Assert 'Sell' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
}
step("Assert 'Hide token' button is displayed") {
onTokenActionsBottomSheet { hideTokenButton.assertIsDisplayed() }
}
}
}
@AllureId("84")
@DisplayName("Action buttons (long tap): check 'Copy address' button")
@Test
fun clickOnCopyAddressButtonTest() {
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
val bitcoinAddress = BITCOIN_ADDRESS
setupHooks(
additionalBeforeSection = {
clearClipboard()
},
additionalAfterSection = {
clearClipboard()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Copy address' button is displayed") {
onTokenActionsBottomSheet { copyAddressButton.assertIsDisplayed() }
}
step("Click on 'Copy address' button") {
onTokenActionsBottomSheet { copyAddressButton.performClick() }
}
step("Assert snack bar message is displayed") {
onMainScreen { snackbarCopiedAddressMessage.assertIsDisplayed() }
}
step("Check clipboard has '$tokenTitle' address '$bitcoinAddress'") {
waitForIdle()
assertClipboardTextEquals(expected = bitcoinAddress)
}
}
}
@AllureId("82")
@DisplayName("Action buttons (long tap): check 'Buy' button")
@Test
fun clickOnBuyButtonTest() {
val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Buy' button is displayed") {
onTokenActionsBottomSheet { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onTokenActionsBottomSheet { buyButton.performClick() }
}
step("Click on 'Confirm' button in 'Dialog'") {
waitForIdle()
onDialog { confirmButton.clickWithAssertion() }
}
step("Assert top app bar title contains '$tokenTitle'") {
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
}
step("Assert fiat currency text field is displayed") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertIsDisplayed() }
}
step("Assert fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
}
step("Assert token amount field is displayed") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenSymbol, substring = true) }
}
step("Assert 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.assertIsDisplayed() }
}
}
}
@AllureId("87")
@DisplayName("Action buttons (long tap): check 'Swap' button")
@Test
fun clickOnSwapButtonTest() {
val tokenTitle = "Ethereum"
val tokenSymbol = "ETH"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Swap' button is displayed") {
onTokenActionsBottomSheet { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenActionsBottomSheet { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert token symbol: '$tokenSymbol' is displayed") {
onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() }
}
}
}
@AllureId("83")
@DisplayName("Action buttons (long tap): check 'Send' button")
@Test
fun clickOnSendButtonTest() {
val tokenTitle = "Ethereum"
val tokenSymbol = "ETH"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Send' button is displayed") {
onTokenActionsBottomSheet { sendButton.assertIsDisplayed() }
}
step("Click on 'Send' button") {
onTokenActionsBottomSheet { sendButton.performClick() }
}
step("Assert amount input text field contains token symbol: '$tokenSymbol'") {
onSendScreen {
amountInputTextField.assertTextContains(value = tokenSymbol, substring = true)
}
}
}
}
@AllureId("86")
@DisplayName("Action buttons (long tap): check 'Receive' button")
@Test
fun clickOnReceiveButtonTest() {
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Receive' button is displayed") {
onTokenActionsBottomSheet { receiveButton.assertIsDisplayed() }
}
step("Click on 'Receive' button") {
onTokenActionsBottomSheet { receiveButton.performClick() }
}
step("Assert 'Token receive warning' bottom sheet is displayed") {
onTokenReceiveWarningBottomSheet { bottomSheet.assertIsDisplayed() }
}
step("Click on 'Got it' button") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
step("Assert bottom sheet with QR code title is displayed") {
onTokenReceiveQrCodeBottomSheet { title.assertIsDisplayed() }
}
step("Assert QR code is displayed") {
onTokenReceiveQrCodeBottomSheet { qrCode.assertIsDisplayed() }
}
step("Assert address title is displayed") {
onTokenReceiveQrCodeBottomSheet { addressTitle.assertIsDisplayed() }
}
step("Assert address is displayed") {
onTokenReceiveQrCodeBottomSheet { address.assertIsDisplayed() }
}
step("Assert 'Copy' button is displayed") {
onTokenReceiveQrCodeBottomSheet { copyButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
}
}
}
@AllureId("85")
@DisplayName("Action buttons (long tap): check 'Sell' button")
@Test
fun clickOnSellButtonTest() {
val tokenTitle = "Ethereum"
val tokenSymbol = "ETH"
val balance = TOTAL_BALANCE
val url = "sell.moonpay.com"
val useWithoutAccount = "Use without an account"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Receive' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
}
step("Click on 'Receive' button") {
onTokenActionsBottomSheet { sellButton.performClick() }
}
step("Assert Chrome Browser is opened") {
ChromeBrowserPageObject { assertChromeIsOpened() }
}
if (ChromeBrowserPageObject.isElementWithTextExists(useWithoutAccount)) {
step("Click on '$useWithoutAccount' button on Chrome browser") {
ChromeBrowserPageObject { clickOnElementWithText(useWithoutAccount) }
}
}
step("Assert url contains: '$url'") {
ChromeBrowserPageObject { assertUrlContains(url) }
}
step("Assert token symbol '$tokenSymbol' is displayed") {
ChromeBrowserPageObject { assertElementWithTextExists(tokenSymbol) }
}
}
}
@AllureId("77")
@DisplayName("Action buttons (long tap): assert 'Sell' button is not displayed if token doesn't support it")
@Test
fun assertSellButtonIsNotDisplayedTest() {
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Long click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Assert 'Sell' button is not displayed") {
onTokenActionsBottomSheet { sellButton.assertIsNotDisplayed() }
}
}
}
}

View file

@ -162,7 +162,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
} }
} }
step("Click 'Hide token' button") { step("Click 'Hide token' button") {
onBottomSheet { hideButton.clickWithAssertion() } onTokenActionsBottomSheet { hideTokenButton.clickWithAssertion() }
} }
step("Click 'Hide' button in dialog") { step("Click 'Hide' button in dialog") {
onDialog { onDialog {

View file

@ -67,7 +67,11 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true) secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true)
} }
override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(createOrderIdKey(customerWalletAddress))
}
override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(createKey(customerWalletAddress)) secureStorage.delete(createKey(customerWalletAddress))
secureStorage.delete(createOrderIdKey(customerWalletAddress)) secureStorage.delete(createOrderIdKey(customerWalletAddress))
} }

View file

@ -49,8 +49,12 @@ internal object AccountDomainModule {
@Singleton @Singleton
fun provideRecoverCryptoPortfolioUseCase( fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
): RecoverCryptoPortfolioUseCase { ): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
)
} }
@Provides @Provides

View file

@ -16,7 +16,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.* import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
@ -454,21 +454,17 @@ internal object WalletsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideYieldSupplyApyFlowUseCase( fun provideYieldSupplyApyFlowUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyFlowUseCase {
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyApyFlowUseCase {
return YieldSupplyApyFlowUseCase( return YieldSupplyApyFlowUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository, yieldSupplyRepository = yieldSupplyRepository,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun provideYieldSupplyApyUpdateUseCase( fun provideYieldSupplyApyUpdateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyUpdateUseCase {
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyApyUpdateUseCase {
return YieldSupplyApyUpdateUseCase( return YieldSupplyApyUpdateUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository, yieldSupplyRepository = yieldSupplyRepository,
) )
} }
} }

View file

@ -4,7 +4,7 @@ import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import dagger.Module import dagger.Module
@ -82,30 +82,54 @@ internal object YieldSupplyDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideYieldSupplyGetTokenStatusUseCase( fun provideYieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository, yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyGetTokenStatusUseCase { ): YieldSupplyGetTokenStatusUseCase {
return YieldSupplyGetTokenStatusUseCase( return YieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository, yieldSupplyRepository = yieldSupplyRepository,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun provideYieldSupplyGetApyUseCase( fun provideYieldSupplyGetApyUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetApyUseCase {
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyGetApyUseCase {
return YieldSupplyGetApyUseCase( return YieldSupplyGetApyUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository, yieldSupplyRepository = yieldSupplyRepository,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun provideYieldSupplyGetChartUseCase( fun provideYieldSupplyGetChartUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetChartUseCase {
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyGetChartUseCase {
return YieldSupplyGetChartUseCase( return YieldSupplyGetChartUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository, yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyIsAvailableUseCase(
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyIsAvailableUseCase {
return YieldSupplyIsAvailableUseCase(
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyActivateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyActivateUseCase {
return YieldSupplyActivateUseCase(
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyDeactivateUseCase(
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyDeactivateUseCase {
return YieldSupplyDeactivateUseCase(
yieldSupplyRepository = yieldSupplyRepository,
) )
} }
} }

View file

@ -160,4 +160,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Pepecoin, PepecoinTestnet -> null Pepecoin, PepecoinTestnet -> null
Hyperliquid, HyperliquidTestnet -> null Hyperliquid, HyperliquidTestnet -> null
Quai, QuaiTestnet -> null Quai, QuaiTestnet -> null
Linea, LineaTestnet -> null
ArbitrumNova -> null
} }

View file

@ -2,6 +2,7 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.referral.api.ReferralComponent
@ -12,6 +13,7 @@ import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.createwalletstart.CreateWalletStartComponent
import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.api.HomeComponent
@ -19,6 +21,7 @@ import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent import com.tangem.features.kyc.KycComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent 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.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
@ -97,6 +100,7 @@ internal class ChildFactory @Inject constructor(
private val usedeskComponentFactory: UsedeskComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory,
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -140,9 +144,15 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
} }
val mode = when (val portfolio = route.portfolioId) {
is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId)
is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId)
null -> ManageTokensMode.None
}
createComponentChild( createComponentChild(
context = context, context = context,
params = ManageTokensComponent.Params(route.userWalletId, source), params = ManageTokensComponent.Params(mode, source),
componentFactory = manageTokensComponentFactory, componentFactory = manageTokensComponentFactory,
) )
} }
@ -200,7 +210,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = OnrampComponent.Params( params = OnrampComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, userWalletId = route.userWalletId,
cryptoCurrency = route.currency, cryptoCurrency = route.currency,
source = route.source, source = route.source,
shouldLaunchSepa = route.shouldLaunchSepa, shouldLaunchSepa = route.shouldLaunchSepa,
@ -274,7 +284,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = TokenDetailsComponent.Params( params = TokenDetailsComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param userWalletId = route.userWalletId,
currency = route.currency, currency = route.currency,
), ),
componentFactory = tokenDetailsComponentFactory, componentFactory = tokenDetailsComponentFactory,
@ -284,7 +294,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = StakingComponent.Params( params = StakingComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId, cryptoCurrencyId = route.cryptoCurrencyId,
yieldId = route.yieldId, yieldId = route.yieldId,
), ),
@ -297,7 +307,7 @@ internal class ChildFactory @Inject constructor(
params = SwapComponent.Params( params = SwapComponent.Params(
currencyFrom = route.currencyFrom, currencyFrom = route.currencyFrom,
currencyTo = route.currencyTo, currencyTo = route.currencyTo,
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, userWalletId = route.userWalletId,
isInitialReverseOrder = route.isInitialReverseOrder, isInitialReverseOrder = route.isInitialReverseOrder,
screenSource = route.screenSource, screenSource = route.screenSource,
), ),
@ -308,7 +318,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = SendComponent.Params( params = SendComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, userWalletId = route.userWalletId,
currency = route.currency, currency = route.currency,
transactionId = route.transactionId, transactionId = route.transactionId,
amount = route.amount, amount = route.amount,
@ -468,6 +478,19 @@ internal class ChildFactory @Inject constructor(
componentFactory = chooseManagedTokensComponentFactory, componentFactory = chooseManagedTokensComponentFactory,
) )
} }
is AppRoute.CreateWalletStart -> {
val mode = when (route.mode) {
AppRoute.CreateWalletStart.Mode.ColdWallet -> CreateWalletStartComponent.Mode.ColdWallet
AppRoute.CreateWalletStart.Mode.HotWallet -> CreateWalletStartComponent.Mode.HotWallet
}
createComponentChild(
context = context,
params = CreateWalletStartComponent.Params(
mode = mode,
),
componentFactory = createWalletStartComponentFactory,
)
}
is AppRoute.CreateWalletSelection -> { is AppRoute.CreateWalletSelection -> {
createComponentChild( createComponentChild(
context = context, context = context,

View file

@ -51,50 +51,25 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class CurrencyDetails( data class CurrencyDetails(
val portfolioId: PortfolioId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
) : AppRoute(path = "/currency_details/${portfolioId.stringValue}/${currency.id.value}") { ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}")
companion object {
operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) = CurrencyDetails(
portfolioId = PortfolioId(userWalletId),
currency = currency,
)
}
}
@Serializable @Serializable
data class Send( data class Send(
val portfolioId: PortfolioId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
val transactionId: String? = null, val transactionId: String? = null,
val amount: String? = null, val amount: String? = null,
val tag: String? = null, val tag: String? = null,
val destinationAddress: String? = null, val destinationAddress: String? = null,
) : AppRoute( ) : AppRoute(
path = "/send/${portfolioId.stringValue}/${currency.id.value}?" + path = "/send/${userWalletId.stringValue}/${currency.id.value}?" +
"&$transactionId" + "&$transactionId" +
"&$amount" + "&$amount" +
"&$tag" + "&$tag" +
"&$destinationAddress", "&$destinationAddress",
) { )
companion object {
operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
transactionId: String? = null,
amount: String? = null,
tag: String? = null,
destinationAddress: String? = null,
) = Send(
portfolioId = PortfolioId(userWalletId),
currency = currency,
transactionId = transactionId,
amount = amount,
tag = tag,
destinationAddress = destinationAddress,
)
}
}
@Serializable @Serializable
data class Details( data class Details(
@ -147,8 +122,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class ManageTokens( data class ManageTokens(
val source: Source, val source: Source,
val userWalletId: UserWalletId? = null, val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId") { ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
enum class Source { enum class Source {
STORIES, STORIES,
@ -199,51 +174,26 @@ sealed class AppRoute(val path: String) : Route {
data class Swap( data class Swap(
val currencyFrom: CryptoCurrency, val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null, val currencyTo: CryptoCurrency? = null,
val portfolioId: PortfolioId, val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false, val isInitialReverseOrder: Boolean = false,
val screenSource: String, val screenSource: String,
) : AppRoute( ) : AppRoute(
path = "/swap" + path = "/swap" +
"/${currencyFrom.id.value}" + "/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" + "/${currencyTo?.id?.value}" +
"/${portfolioId.stringValue}" + "/${userWalletId.stringValue}" +
"/$isInitialReverseOrder", "/$isInitialReverseOrder",
) { )
companion object {
operator fun invoke(
userWalletId: UserWalletId,
currencyFrom: CryptoCurrency,
currencyTo: CryptoCurrency? = null,
isInitialReverseOrder: Boolean = false,
screenSource: String,
) = Swap(
portfolioId = PortfolioId(userWalletId),
currencyFrom = currencyFrom,
currencyTo = currencyTo,
isInitialReverseOrder = isInitialReverseOrder,
screenSource = screenSource,
)
}
}
@Serializable @Serializable
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")
@Serializable @Serializable
data class Staking( data class Staking(
val portfolioId: PortfolioId, val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID, val cryptoCurrencyId: CryptoCurrency.ID,
val yieldId: String, val yieldId: String,
) : AppRoute(path = "/staking/${portfolioId.stringValue}/${cryptoCurrencyId.value}/$yieldId") { ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId")
companion object {
operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yieldId: String) =
Staking(
portfolioId = PortfolioId(userWalletId),
cryptoCurrencyId = cryptoCurrencyId,
yieldId = yieldId,
)
}
}
@Serializable @Serializable
data class PushNotification( data class PushNotification(
@ -287,25 +237,11 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class Onramp( data class Onramp(
val source: OnrampSource, val source: OnrampSource,
val portfolioId: PortfolioId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
val shouldLaunchSepa: Boolean = false, val shouldLaunchSepa: Boolean = false,
) : AppRoute(path = "/onramp/${portfolioId.stringValue}/${currency.symbol}"), RouteBundleParams { ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer()) override fun getBundle(): Bundle = bundle(serializer())
companion object {
operator fun invoke(
source: OnrampSource,
userWalletId: UserWalletId,
currency: CryptoCurrency,
launchSepa: Boolean = false,
) = Onramp(
source = source,
portfolioId = PortfolioId(userWalletId),
currency = currency,
shouldLaunchSepa = launchSepa,
)
}
} }
@Serializable @Serializable
@ -375,6 +311,16 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
object CreateWalletSelection : AppRoute(path = "/create_wallet_selection") object CreateWalletSelection : AppRoute(path = "/create_wallet_selection")
@Serializable
data class CreateWalletStart(
val mode: Mode,
) : AppRoute(path = "/create_wallet_start") {
enum class Mode {
ColdWallet,
HotWallet,
}
}
@Serializable @Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")

View file

@ -0,0 +1,53 @@
package com.tangem.common.ui.account
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
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.text.TextStyle
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Displays account name with icon
*
* @param name account name
* @param icon portfolio account icon model
* @param iconSize portfolio account icon size
* @param nameStyle account name style
* @param nameColor account name color
* @see AccountIcon
*/
@Composable
fun AccountLabel(
name: TextReference,
icon: CryptoPortfolioIconUM,
iconSize: AccountIconSize,
modifier: Modifier = Modifier,
nameStyle: TextStyle = TangemTheme.typography.subtitle2,
nameColor: Color = TangemTheme.colors.text.tertiary,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
AccountIcon(
name = name,
icon = icon,
size = iconSize,
)
Text(
text = name.resolveReference(),
style = nameStyle,
color = nameColor,
maxLines = 1,
)
}
}

View file

@ -33,7 +33,6 @@ class AccountPortfolioItemUMConverter(
endIcon = endIcon, endIcon = endIcon,
onClick = { onClick(value.accountId) }, onClick = { onClick(value.accountId) },
imageState = getImageState(value), imageState = getImageState(value),
label = null,
) )
} }
} }

View file

@ -13,21 +13,17 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.common.ui.amountScreen.ui.amountField
import com.tangem.common.ui.amountScreen.ui.amountFieldV2 import com.tangem.common.ui.amountScreen.ui.amountFieldV2
import com.tangem.common.ui.amountScreen.ui.buttons
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
/** /**
* Amount screen with field * Amount screen with field
* @param amountState amount state * @param amountState amount state
* @param isBalanceHidden flag hidden balances
* @param clickIntents amount screen clicks * @param clickIntents amount screen clicks
*/ */
@Composable @Composable
fun AmountScreenContent( fun AmountScreenContent(
amountState: AmountState, amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: AmountScreenClickIntents, clickIntents: AmountScreenClickIntents,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
extraContent: (@Composable () -> Unit)? = null, extraContent: (@Composable () -> Unit)? = null,
@ -38,32 +34,17 @@ fun AmountScreenContent(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
if (amountState.isRedesignEnabled) { amountFieldV2(
amountFieldV2( amountState = amountState,
amountState = amountState, onValueChange = clickIntents::onAmountValueChange,
onValueChange = clickIntents::onAmountValueChange, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onCurrencyChange = clickIntents::onCurrencyChangeClick,
onCurrencyChange = clickIntents::onCurrencyChangeClick, onMaxAmountClick = clickIntents::onMaxValueClick,
onMaxAmountClick = clickIntents::onMaxValueClick, )
) if (extraContent != null) {
if (extraContent != null) { item("EXTRA_CONTENT_KEY") {
item("EXTRA_CONTENT_KEY") { extraContent()
extraContent()
}
} }
} else if (amountState is AmountState.Data) {
amountField(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
selectedButton = amountState.selectedButton,
)
} }
} }
} }
@ -78,7 +59,6 @@ private fun SendAmountContentPreview(
TangemThemePreview { TangemThemePreview {
AmountScreenContent( AmountScreenContent(
amountState = amountState, amountState = amountState,
isBalanceHidden = false,
clickIntents = AmountScreenClickIntentsStub, clickIntents = AmountScreenClickIntentsStub,
) )
} }

View file

@ -38,7 +38,6 @@ class AmountCurrencyTransformer(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
), ),
), ),
selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value },
) )
} }
} }

View file

@ -70,10 +70,9 @@ class AmountReduceByTransformer(
error = when { error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> { isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount val minimumAmount = minimumTransactionAmount.amount.format {
?.amount crypto(cryptoCurrencyStatus.currency)
?.format { crypto(cryptoCurrencyStatus.currency) } }
.orEmpty()
resourceReference( resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text, R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount), wrappedList(minimumAmount, minimumAmount),

View file

@ -64,10 +64,9 @@ class AmountReduceToTransformer(
error = when { error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> { isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount val minimumAmount = minimumTransactionAmount.amount.format {
?.amount crypto(cryptoCurrencyStatus.currency)
?.format { crypto(cryptoCurrencyStatus.currency) } }
.orEmpty()
resourceReference( resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text, R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount), wrappedList(minimumAmount, minimumAmount),

View file

@ -1,104 +1,35 @@
package com.tangem.common.ui.amountScreen.converters package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2
import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.persistentListOf
/**
* Converts initial [String] to [AmountState]
*
* @property clickIntents amount screen clicks
* @property appCurrencyProvider selected app currency provider
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
* @property iconStateConverter currency icon converter
*/
@Deprecated("Use AmountStateConverterV2")
class AmountStateConverter(
private val clickIntents: AmountScreenClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverter(
clickIntents = clickIntents,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
)
}
override fun convert(value: AmountParameters): AmountState {
val appCurrency = appCurrencyProvider()
val status = cryptoCurrencyStatusProvider()
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(status.currency) }
val hasNoFeeRate = status.value.fiatRate.isNullOrZero()
return AmountState.Data(
title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
availableBalanceCrypto = stringReference(crypto),
availableBalanceFiat = stringReference(fiat),
tokenName = stringReference(status.currency.name),
tokenIconState = iconStateConverter.convert(status),
amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false,
appCurrency = appCurrency,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),
iconState = iconStateConverter.convertCustom(
value = status,
forceGrayscale = hasNoFeeRate,
showCustomTokenBadge = false,
),
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
isSegmentedButtonsEnabled = !hasNoFeeRate,
selectedButton = 0,
isRedesignEnabled = false,
)
}
}
/** /**
* Converts initial [String] to [AmountState] * Converts initial [String] to [AmountState]
* *
* @property clickIntents amount screen clicks * @property clickIntents amount screen clicks
* @property appCurrency selected app currency * @property appCurrency selected app currency
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status * @property cryptoCurrencyStatus current cryptocurrency status
* @property maxEnterAmount max enter amount data
* @property iconStateConverter currency icon converter * @property iconStateConverter currency icon converter
* @property isBalanceHidden is balance hidden status * @property isBalanceHidden is balance hidden status
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class AmountStateConverterV2( class AmountStateConverter(
private val clickIntents: AmountScreenClickIntents, private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -108,7 +39,7 @@ class AmountStateConverterV2(
) : Converter<AmountParameters, AmountState> { ) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverterV2( AmountFieldConverter(
clickIntents = clickIntents, clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency, appCurrency = appCurrency,
@ -118,19 +49,13 @@ class AmountStateConverterV2(
override fun convert(value: AmountParameters): AmountState { override fun convert(value: AmountParameters): AmountState {
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) { if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
return AmountState.Empty(isRedesignEnabled = true) return AmountState.Empty
} }
return AmountState.Data( return AmountState.Data(
title = value.title, title = value.title,
availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
).orMaskWithStars(isBalanceHidden),
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) { availableBalanceFiat = if (isBalanceHidden) {
TextReference.EMPTY TextReference.EMPTY
@ -145,25 +70,6 @@ class AmountStateConverterV2(
amountTextField = amountFieldConverter.convert(value.value), amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
appCurrency = appCurrency, appCurrency = appCurrency,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference(cryptoCurrencyStatus.currency.symbol),
iconState = iconStateConverter.convertCustom(
value = cryptoCurrencyStatus,
forceGrayscale = noFeeRate,
showCustomTokenBadge = false,
),
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
isRedesignEnabled = true,
) )
} }
} }

View file

@ -2,7 +2,10 @@ package com.tangem.common.ui.amountScreen.converters.field
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
@ -32,14 +35,7 @@ class AmountBoundaryUpdateTransformer(
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
return prevState.copy( return prevState.copy(
availableBalance = availableBalance.orMaskWithStars(isBalanceHidden),
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) { availableBalanceFiat = if (isBalanceHidden) {
TextReference.EMPTY TextReference.EMPTY

View file

@ -13,75 +13,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.convertToAmount import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero import com.tangem.utils.isNullOrZero
import java.math.BigDecimal import java.math.BigDecimal
/**
* Converts initial [String] to [AmountFieldModel]
*
* @property clickIntents amount screen clicks
* @property appCurrencyProvider selected app currency provider
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
*/
@Deprecated("Use AmountFieldConverterV2")
class AmountFieldConverter(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<String, AmountFieldModel> {
override fun convert(value: String): AmountFieldModel {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = when {
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()
fiatValue to fiatDecimal
}
}
val isDoneActionEnabled = !cryptoDecimal.isNullOrZero()
return AmountFieldModel(
value = value,
fiatValue = fiatValue,
onValueChange = clickIntents::onAmountValueChange,
keyboardOptions = KeyboardOptions(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onAmountNext() },
),
isFiatValue = false,
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()),
isError = false,
isWarning = false,
error = TextReference.EMPTY,
isFiatUnavailable = fiatRate == null,
isValuePasted = false,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
}
private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount(
currencySymbol = appCurrency.symbol,
value = fiatValue,
decimals = FIAT_DECIMALS,
type = AmountType.FiatType(appCurrency.code),
)
private companion object {
private const val FIAT_DECIMALS = 2
}
}
/** /**
* Converts initial [String] to [AmountFieldModel] * Converts initial [String] to [AmountFieldModel]
* *
@ -89,7 +24,7 @@ class AmountFieldConverter(
* @property appCurrency selected app currency * @property appCurrency selected app currency
* @property cryptoCurrencyStatus current cryptocurrency status * @property cryptoCurrencyStatus current cryptocurrency status
*/ */
class AmountFieldConverterV2( class AmountFieldConverter(
private val clickIntents: AmountScreenClickIntents, private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,

View file

@ -54,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer(
isError = isLessThanMinimumIfProvided, isError = isLessThanMinimumIfProvided,
error = when { error = when {
isLessThanMinimumIfProvided -> { isLessThanMinimumIfProvided -> {
val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) } val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
resourceReference( resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text, R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount), wrappedList(minimumAmount, minimumAmount),

View file

@ -1,21 +0,0 @@
package com.tangem.common.ui.amountScreen.models
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
/**
* Segmented buttons config
*
* @param title button title
* @param iconState currency icon state
* @param iconUrl currency icon url
* @param isFiat is fiat currency
*/
@Immutable
data class AmountSegmentedButtonsConfig(
val title: TextReference,
val iconState: CurrencyIconState? = null,
val iconUrl: String? = null,
val isFiat: Boolean,
)

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal import java.math.BigDecimal
/** Model for amount state */ /** Model for amount state */
@ -12,18 +11,13 @@ import java.math.BigDecimal
sealed class AmountState { sealed class AmountState {
abstract val isPrimaryButtonEnabled: Boolean abstract val isPrimaryButtonEnabled: Boolean
abstract val isRedesignEnabled: Boolean
/** /**
* @param isPrimaryButtonEnabled indicates if next state button enabled * @param isPrimaryButtonEnabled indicates if next state button enabled
* @param title title * @param title title
* @param availableBalance user crypto currency balance with fiat balance
* @param availableBalanceCrypto user crypto currency balance in crypto * @param availableBalanceCrypto user crypto currency balance in crypto
* @param availableBalanceFiat user crypto currency balance in fiat * @param availableBalanceFiat user crypto currency balance in fiat
* @param tokenIconState crypto currency icon state * @param tokenIconState crypto currency icon state
* @param segmentedButtonConfig currency switcher config
* @param selectedButton selected currency index
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
* @param amountTextField amount field state * @param amountTextField amount field state
* @param appCurrency app currency * @param appCurrency app currency
* @param isEditingDisabled indicated whether amount is editable * @param isEditingDisabled indicated whether amount is editable
@ -32,17 +26,11 @@ sealed class AmountState {
*/ */
data class Data( data class Data(
override val isPrimaryButtonEnabled: Boolean, override val isPrimaryButtonEnabled: Boolean,
override val isRedesignEnabled: Boolean,
val title: TextReference, val title: TextReference,
@Deprecated("Remove with SEND_REDESIGNED toggle")
val availableBalance: TextReference,
val availableBalanceCrypto: TextReference, val availableBalanceCrypto: TextReference,
val availableBalanceFiat: TextReference, val availableBalanceFiat: TextReference,
val tokenName: TextReference, val tokenName: TextReference,
val tokenIconState: CurrencyIconState, val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel, val amountTextField: AmountFieldModel,
val appCurrency: AppCurrency, val appCurrency: AppCurrency,
val isEditingDisabled: Boolean = false, val isEditingDisabled: Boolean = false,
@ -50,8 +38,7 @@ sealed class AmountState {
val isIgnoreReduce: Boolean = false, val isIgnoreReduce: Boolean = false,
) : AmountState() ) : AmountState()
data class Empty( data object Empty : AmountState() {
override val isPrimaryButtonEnabled: Boolean = false, override val isPrimaryButtonEnabled: Boolean = false
override val isRedesignEnabled: Boolean, }
) : AmountState()
} }

View file

@ -3,7 +3,6 @@ package com.tangem.common.ui.amountScreen.preview
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
@ -12,31 +11,18 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.AmountType
import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal import java.math.BigDecimal
object AmountStatePreviewData { object AmountStatePreviewData {
val emptyState = AmountState.Empty(isRedesignEnabled = true) val emptyState = AmountState.Empty
val amountState = AmountState.Data( val amountState = AmountState.Data(
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"), title = stringReference("Family Wallet"),
availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
availableBalanceFiat = stringReference("1 232 129,12 \$"), availableBalanceFiat = stringReference("1 232 129,12 \$"),
tokenIconState = CurrencyIconState.Loading, tokenIconState = CurrencyIconState.Loading,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference("USDT"),
iconState = CurrencyIconState.Locked,
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference("USD"),
isFiat = true,
),
),
appCurrency = AppCurrency.Default, appCurrency = AppCurrency.Default,
tokenName = stringReference("Tether"), tokenName = stringReference("Tether"),
amountTextField = AmountFieldModel( amountTextField = AmountFieldModel(
@ -65,12 +51,9 @@ object AmountStatePreviewData {
isValuePasted = false, isValuePasted = false,
onValuePastedTriggerDismiss = {}, onValuePastedTriggerDismiss = {},
), ),
isSegmentedButtonsEnabled = true,
selectedButton = 0,
isRedesignEnabled = false,
) )
val amountWithValueState = amountState.copy( private val amountWithValueState = amountState.copy(
amountTextField = amountState.amountTextField.copy( amountTextField = amountState.amountTextField.copy(
value = "100.00", value = "100.00",
cryptoAmount = amountState.amountTextField.cryptoAmount.copy( cryptoAmount = amountState.amountTextField.cryptoAmount.copy(
@ -84,16 +67,10 @@ object AmountStatePreviewData {
) )
val amountStateV2 = amountState.copy( val amountStateV2 = amountState.copy(
isRedesignEnabled = true,
availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"), availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"),
) )
val amountWithValueFiatState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
)
val amountStateV2WithoutRates = amountState.copy( val amountStateV2WithoutRates = amountState.copy(
amountTextField = amountState.amountTextField.copy( amountTextField = amountState.amountTextField.copy(
fiatAmount = amountState.amountTextField.fiatAmount.copy( fiatAmount = amountState.amountTextField.fiatAmount.copy(

View file

@ -11,21 +11,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseAmountBlockTestTags
@Composable @Composable
fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) { fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) {
@ -59,7 +60,16 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing16), .padding(TangemTheme.dimens.spacing16),
) { ) {
CurrencyIcon(state = amountState.tokenIconState) Text(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(20.dp)
CurrencyIcon(
state = amountState.tokenIconState,
iconSize = 40.dp,
)
ResizableText( ResizableText(
text = firstAmount, text = firstAmount,
style = TangemTheme.typography.h2, style = TangemTheme.typography.h2,
@ -68,8 +78,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
maxLines = 1, maxLines = 1,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24) .padding(top = TangemTheme.dimens.spacing24),
.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
) )
Text( Text(
text = secondAmount, text = secondAmount,
@ -78,8 +87,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8) .padding(top = TangemTheme.dimens.spacing8),
.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
) )
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseAmountBlockTestTags
@Composable @Composable
fun AmountBlockV2( fun AmountBlockV2(
@ -133,6 +135,7 @@ private fun AmountBlockV2(
style = TangemTheme.typography.h2, style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
maxLines = 1, maxLines = 1,
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
) )
Row( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -142,6 +145,7 @@ private fun AmountBlockV2(
style = TangemTheme.typography.body2, style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
maxLines = 1, maxLines = 1,
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
) )
extraContent() extraContent()
} }

View file

@ -1,126 +0,0 @@
package com.tangem.common.ui.amountScreen.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendScreenTestTags
import kotlinx.collections.immutable.PersistentList
private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
clickIntents: AmountScreenClickIntents,
isSegmentedButtonsEnabled: Boolean,
selectedButton: Int,
) {
item(
key = AMOUNT_BUTTONS_KEY,
) {
val hapticFeedback = LocalHapticFeedback.current
Row {
if (segmentedButtonConfig.isNotEmpty()) {
SegmentedButtons(
modifier = Modifier
.weight(1f)
.height(TangemTheme.dimens.size40),
config = segmentedButtonConfig,
showIndication = false,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onCurrencyChangeClick(it.isFiat)
},
initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton),
isEnabled = isSegmentedButtonsEnabled,
) {
AmountCurrencyButton(
button = it,
isSegmentedButtonsEnabled = isSegmentedButtonsEnabled,
)
}
} else {
SpacerWMax()
}
Text(
text = stringResourceSafe(R.string.send_max_amount),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.height(TangemTheme.dimens.size40)
.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26))
.background(TangemTheme.colors.button.secondary)
.clickable {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()
}
.padding(
vertical = TangemTheme.dimens.spacing10,
horizontal = TangemTheme.dimens.spacing34,
)
.testTag(SendScreenTestTags.MAX_BUTTON),
)
}
}
}
@Composable
private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing10,
)
.testTag(SendScreenTestTags.CURRENCY_BUTTON),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
val iconModifier = Modifier
.size(TangemTheme.dimens.size18)
.padding(horizontal = TangemTheme.dimens.spacing1)
if (button.isFiat) {
FiatIcon(
url = button.iconUrl,
size = TangemTheme.dimens.size18,
isGrayscale = !isSegmentedButtonsEnabled,
modifier = iconModifier.testTag(SendScreenTestTags.FIAT_ICON),
)
} else if (button.iconState != null) {
CurrencyIcon(
state = button.iconState,
shouldDisplayNetwork = false,
modifier = iconModifier.testTag(SendScreenTestTags.CURRENCY_ICON),
)
}
Text(
text = button.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing8,
),
)
}
}

View file

@ -1,160 +0,0 @@
package com.tangem.common.ui.amountScreen.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@Composable
internal fun AmountField(
amountField: AmountFieldModel,
appCurrencyCode: String,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = amountField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
val (primaryAmount, primaryValue) = if (isFiatValue) {
amountField.fiatAmount to amountField.fiatValue
} else {
amountField.cryptoAmount to amountField.value
}
val requester = remember { FocusRequester() }
val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
visualTransformation = AmountVisualTransformation(
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
symbolColor = symbolColor,
),
onValueChange = onValueChange,
keyboardOptions = amountField.keyboardOptions,
keyboardActions = amountField.keyboardActions,
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isAutoResize = true,
isValuePasted = amountField.isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.padding(
top = TangemTheme.dimens.spacing24,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
)
.requiredHeightIn(min = TangemTheme.dimens.size32),
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
AmountSecondary(amountField, appCurrencyCode)
}
@Composable
private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) {
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.padding(
top = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
) {
val text = if (amountField.isFiatValue) {
secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) }
} else {
secondaryAmount.value.format {
fiat(
fiatCurrencySymbol = secondaryAmount.currencySymbol,
fiatCurrencyCode = appCurrencyCode,
)
}
}
Text(
text = text,
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(TopCenter)
.padding(bottom = TangemTheme.dimens.spacing32)
.testTag(SendScreenTestTags.SECONDARY_AMOUNT),
)
AmountFieldError(
isError = amountField.isError,
isWarning = amountField.isWarning,
error = amountField.error,
modifier = Modifier
.align(BottomCenter)
.padding(
top = TangemTheme.dimens.spacing20,
bottom = TangemTheme.dimens.spacing12,
),
)
}
}
@Composable
private fun AmountFieldError(
isError: Boolean,
isWarning: Boolean,
error: TextReference,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isError || isWarning,
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier,
) {
val errorText = remember(this, error) { error }
val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention
Text(
text = errorText.resolveReference(),
style = TangemTheme.typography.caption2,
color = color,
textAlign = TextAlign.Center,
)
}
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
@ -25,7 +24,6 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -33,60 +31,6 @@ import com.tangem.core.ui.test.SendScreenTestTags
private const val AMOUNT_FIELD_KEY = "amountFieldKey" private const val AMOUNT_FIELD_KEY = "amountFieldKey"
internal fun LazyListScope.amountField(
amountState: AmountState.Data,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
item(key = AMOUNT_FIELD_KEY) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action),
) {
Text(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing14)
.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference()
AnimatedContent(
targetState = balance,
label = "Hide Balance Animation",
) {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2)
.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT),
)
}
CurrencyIcon(
state = amountState.tokenIconState,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32),
)
AmountField(
amountField = amountState.amountTextField,
appCurrencyCode = amountState.appCurrency.code,
onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
)
}
}
}
internal fun LazyListScope.amountFieldV2( internal fun LazyListScope.amountFieldV2(
amountState: AmountState, amountState: AmountState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -118,6 +62,7 @@ internal fun LazyListScope.amountFieldV2(
text = amountState.title.resolveReference(), text = amountState.title.resolveReference(),
style = TangemTheme.typography.subtitle2, style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
) )
} }
AmountFieldV2( AmountFieldV2(
@ -175,7 +120,8 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
indication = ripple(), indication = ripple(),
onClick = onMaxAmountClick, onClick = onMaxAmountClick,
) )
.padding(horizontal = 12.dp, vertical = 4.dp), .padding(horizontal = 12.dp, vertical = 4.dp)
.testTag(SendScreenTestTags.MAX_BUTTON),
) )
} }
} }
@ -183,46 +129,53 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
@Composable @Composable
private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) { private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) {
AnimatedContent( AnimatedContent(
targetState = amountUM !is AmountState.Data, targetState = amountUM,
modifier = modifier, modifier = modifier,
) { isContent -> ) { currentAmount ->
if (isContent) { Column(
Column( verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp), ) {
) { when (currentAmount) {
TextShimmer( is AmountState.Data -> {
style = TangemTheme.typography.subtitle2, val amountUM = amountUM as AmountState.Data
modifier = Modifier.width(56.dp), Text(
) text = amountUM.tokenName.resolveReference(),
TextShimmer( style = TangemTheme.typography.subtitle2,
style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1,
modifier = Modifier.width(72.dp), maxLines = 1,
) modifier = Modifier.testTag(SendScreenTestTags.TOKEN_NAME),
}
} else {
val amountUM = amountUM as AmountState.Data
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = amountUM.tokenName.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
Row {
EllipsisText(
text = amountUM.availableBalanceCrypto.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length),
modifier = Modifier.weight(1f, fill = false),
) )
EllipsisText( Row {
text = amountUM.availableBalanceFiat.resolveReference(), EllipsisText(
text = amountUM.availableBalanceCrypto.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(
amountUM.amountTextField.cryptoAmount.currencySymbol.length,
),
modifier = Modifier
.weight(1f, fill = false)
.testTag(SendScreenTestTags.PRIMARY_AMOUNT),
)
EllipsisText(
text = amountUM.availableBalanceFiat.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(
amountUM.amountTextField.fiatAmount.currencySymbol.length,
),
modifier = Modifier.testTag(SendScreenTestTags.SECONDARY_AMOUNT),
)
}
}
AmountState.Empty -> {
TextShimmer(
style = TangemTheme.typography.subtitle2,
modifier = Modifier.width(56.dp),
)
TextShimmer(
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary, modifier = Modifier.width(72.dp),
ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.fiatAmount.currencySymbol.length),
) )
} }
} }

View file

@ -55,7 +55,6 @@ fun NavigationButtonsBlock(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) { ) {
PreviousButton(state?.prevButton)
NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
} }
} }

View file

@ -8,7 +8,6 @@ sealed class NavigationButtonsState {
data class Data( data class Data(
val primaryButton: NavigationButton?, val primaryButton: NavigationButton?,
val prevButton: NavigationButton?,
val extraButtons: Pair<NavigationButton, NavigationButton>?, val extraButtons: Pair<NavigationButton, NavigationButton>?,
val txUrl: String? = null, val txUrl: String? = null,
val onTextClick: (String) -> Unit, val onTextClick: (String) -> Unit,

View file

@ -3,7 +3,6 @@ package com.tangem.common.ui.navigationButtons.preview
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
internal object NavigationButtonsPreview { internal object NavigationButtonsPreview {
@ -26,16 +25,6 @@ internal object NavigationButtonsPreview {
onClick = {}, onClick = {},
) )
private val prev = NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isSecondary = true,
isIconVisible = true,
shouldShowProgress = false,
isEnabled = true,
onClick = {},
)
private val finished = NavigationButton( private val finished = NavigationButton(
textReference = resourceReference(R.string.common_close), textReference = resourceReference(R.string.common_close),
isSecondary = false, isSecondary = false,
@ -47,7 +36,6 @@ internal object NavigationButtonsPreview {
val allButtons = NavigationButtonsState.Data( val allButtons = NavigationButtonsState.Data(
primaryButton = finished, primaryButton = finished,
prevButton = prev,
extraButtons = extraButtons, extraButtons = extraButtons,
txUrl = "https://tangem.com", txUrl = "https://tangem.com",
onTextClick = {}, onTextClick = {},

View file

@ -7,7 +7,9 @@ import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
@ -15,6 +17,8 @@ import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DASH_SIGN
@ -35,10 +39,13 @@ import java.math.BigDecimal
*/ */
class TokenItemStateConverter( class TokenItemStateConverter(
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val apyMap: Map<String, String> = emptyMap(),
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it) CryptoCurrencyToIconStateConverter().convert(it)
}, },
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = Companion::createTitleState, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = {
createTitleState(it, apyMap)
},
private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = { private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = {
createSubtitleState(it, appCurrency) createSubtitleState(it, appCurrency)
}, },
@ -144,7 +151,10 @@ class TokenItemStateConverter(
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data) private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero() ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState { private fun createTitleState(
currencyStatus: CryptoCurrencyStatus,
apyMap: Map<String, String>,
): TokenItemState.TitleState {
return when (val value = currencyStatus.value) { return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading, is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.MissedDerivation,
@ -158,14 +168,33 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.NoAccount,
-> { -> {
val earnApyText = resolveEarnApy(currencyStatus, apyMap)?.let { apy ->
resourceReference(
R.string.yield_module_earn_badge,
wrappedList(apy),
)
}
TokenItemState.TitleState.Content( TokenItemState.TitleState.Content(
text = stringReference(currencyStatus.currency.name), text = stringReference(currencyStatus.currency.name),
hasPending = value.hasCurrentNetworkTransactions, hasPending = value.hasCurrentNetworkTransactions,
earnApy = earnApyText,
) )
} }
} }
} }
private fun resolveEarnApy(cryptoCurrencyStatus: CryptoCurrencyStatus, apyMap: Map<String, String>): String? {
if (apyMap.isEmpty()) return null
val isYieldSupplyActive = (cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded)
?.yieldSupplyStatus?.isActive == true
if (isYieldSupplyActive) return null
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return null
return apyMap[token.yieldSupplyKey()]
}
private fun createSubtitleState( private fun createSubtitleState(
currencyStatus: CryptoCurrencyStatus, currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency, appCurrency: AppCurrency,

View file

@ -36,9 +36,6 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -76,8 +73,6 @@ fun UserWalletItem(
balance = state.balance, balance = state.balance,
) )
state.label?.let { Label(it) }
when (state.endIcon) { when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> Unit UserWalletItemUM.EndIcon.None -> Unit
UserWalletItemUM.EndIcon.Arrow -> { UserWalletItemUM.EndIcon.Arrow -> {
@ -94,6 +89,13 @@ fun UserWalletItem(
contentDescription = null, contentDescription = null,
) )
} }
UserWalletItemUM.EndIcon.Warning -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_circle_24),
tint = TangemTheme.colors.icon.warning,
contentDescription = null,
)
}
} }
} }
} }
@ -316,10 +318,7 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Mobile Wallet"), name = stringReference("Mobile Wallet"),
information = getInformation(cardCount = 1), information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked, balance = UserWalletItemUM.Balance.Locked,
label = LabelUM( endIcon = UserWalletItemUM.EndIcon.Warning,
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
isEnabled = true, isEnabled = true,
onClick = {}, onClick = {},
), ),

View file

@ -2,10 +2,7 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
@ -52,7 +49,6 @@ class UserWalletItemUMConverter(
endIcon = endIcon, endIcon = endIcon,
onClick = { onClick(value.walletId) }, onClick = { onClick(value.walletId) },
imageState = artwork, imageState = artwork,
label = getLabelOrNull(userWallet = this),
) )
} }
} }
@ -61,17 +57,6 @@ class UserWalletItemUMConverter(
return isAuthMode || userWallet.isLocked.not() return isAuthMode || userWallet.isLocked.not()
} }
private fun getLabelOrNull(userWallet: UserWallet): LabelUM? {
return if (isAuthMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
}
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) { val text = when (userWallet) {
is UserWallet.Cold -> { is UserWallet.Cold -> {

View file

@ -2,7 +2,6 @@ package com.tangem.common.ui.userwallet.state
import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import javax.annotation.concurrent.Immutable import javax.annotation.concurrent.Immutable
@ -17,12 +16,13 @@ data class UserWalletItemUM(
val isEnabled: Boolean, val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None, val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit, val onClick: () -> Unit,
val label: LabelUM? = null,
) { ) {
enum class EndIcon { enum class EndIcon {
None, None,
Arrow, Arrow,
Checkmark, Checkmark,
Warning,
} }
sealed class Balance { sealed class Balance {

View file

@ -27,10 +27,6 @@
"name": "alephium", "name": "alephium",
"version": "5.21.0" "version": "5.21.0"
}, },
{
"name": "scroll",
"version": "undefined"
},
{ {
"name": "zklink", "name": "zklink",
"version": "undefined" "version": "undefined"

View file

@ -43,7 +43,7 @@ internal class TangemPay(
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD, environment = ApiEnvironment.PROD,
baseUrl = "https://api.paera.com/bff/", baseUrl = "https://api.us.paera.com/bff/",
headers = createHeaders(), headers = createHeaders(),
) )

View file

@ -156,7 +156,7 @@ interface TangemTechApi {
@Path("walletId") walletId: String, @Path("walletId") walletId: String,
@Header("If-Match") eTag: String, @Header("If-Match") eTag: String,
@Body body: SaveWalletAccountsResponse, @Body body: SaveWalletAccountsResponse,
): ApiResponse<Unit> ): ApiResponse<GetWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived") @GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts( suspend fun getWalletArchivedAccounts(

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
@ -15,13 +15,13 @@ import retrofit2.http.Query
interface YieldSupplyApi { interface YieldSupplyApi {
@GET("api/v1/yield/markets") @GET("api/v1/yield/markets")
suspend fun getYieldMarkets(@Query("chainId") chainId: Int? = null): ApiResponse<YieldMarketsResponse> suspend fun getYieldMarkets(@Query("chainId") chainId: String? = null): ApiResponse<YieldMarketsResponse>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}") @GET("api/v1/yield/token/{chainId}/{tokenAddress}")
suspend fun getYieldTokenStatus( suspend fun getYieldTokenStatus(
@Path("chainId") chainId: Int, @Path("chainId") chainId: Int,
@Path("tokenAddress") tokenAddress: String, @Path("tokenAddress") tokenAddress: String,
): ApiResponse<YieldTokenStatusResponse> ): ApiResponse<YieldSupplyMarketTokenDto>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart") @GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart")
suspend fun getYieldTokenChart( suspend fun getYieldTokenChart(

View file

@ -2,21 +2,9 @@ package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class YieldMarketsResponse( data class YieldMarketsResponse(
@Json(name = "tokens") val marketDtos: List<MarketDto>, @Json(name = "tokens") val marketDtos: List<YieldSupplyMarketTokenDto>,
@Json(name = "lastUpdatedAt") val lastUpdated: String, @Json(name = "lastUpdatedAt") val lastUpdated: String,
) { )
@JsonClass(generateAdapter = true)
data class MarketDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null,
@Json(name = "apy") val apy: BigDecimal,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "chainId") val chainId: Int? = null,
)
}

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class YieldTokenStatusResponse( data class YieldSupplyMarketTokenDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null, @Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null, @Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null, @Json(name = "tokenName") val tokenName: String? = null,

View file

@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.datasource.utils.SerializeNulls
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse( data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>, @Json(name = "accounts") val accounts: List<AccountDTO>,
) ) {
@SerializeNulls
@JsonClass(generateAdapter = true)
data class AccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String?,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
)
companion object {
operator fun invoke(accounts: List<WalletAccountDTO>): SaveWalletAccountsResponse {
return SaveWalletAccountsResponse(
accounts = accounts.map { accountDto ->
AccountDTO(
id = accountDto.id,
name = accountDto.name,
derivationIndex = accountDto.derivationIndex,
icon = accountDto.icon,
iconColor = accountDto.iconColor,
)
},
)
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -28,6 +29,7 @@ class MoshiModule {
@NetworkMoshi @NetworkMoshi
fun provideNetworkMoshi(): Moshi { fun provideNetworkMoshi(): Moshi {
return Moshi.Builder() return Moshi.Builder()
.add(SerializeNullsFactory)
.add( .add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type") PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public") .withSubtype(ProviderModel.Public::class.java, "public")

View file

@ -4,11 +4,11 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -34,7 +34,7 @@ object YieldSupplyModule {
persistenceStore = DataStoreFactory.create( persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer( serializer = MoshiDataStoreSerializer(
moshi = moshi, moshi = moshi,
types = listTypes<YieldMarketToken>(), types = listTypes<YieldSupplyMarketTokenDto>(),
defaultValue = emptyList(), defaultValue = emptyList(),
), ),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") }, produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },

View file

@ -12,5 +12,7 @@ interface TangemPayStorage {
suspend fun getOrderId(customerWalletAddress: String): String? suspend fun getOrderId(customerWalletAddress: String): String?
suspend fun clear(customerWalletAddress: String) suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String)
} }

View file

@ -1,21 +1,21 @@
package com.tangem.datasource.local.yieldsupply package com.tangem.datasource.local.yieldsupply
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
internal class DefaultYieldMarketsStore( internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketToken>>, private val persistenceStore: DataStore<List<YieldSupplyMarketTokenDto>>,
) : YieldMarketsStore { ) : YieldMarketsStore {
override fun get(): Flow<List<YieldMarketToken>> = persistenceStore.data override fun get(): Flow<List<YieldSupplyMarketTokenDto>> = persistenceStore.data
override suspend fun getSyncOrNull(): List<YieldMarketToken>? { override suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>? {
return persistenceStore.data.firstOrNull() return persistenceStore.data.firstOrNull()
} }
override suspend fun store(items: List<YieldMarketToken>) { override suspend fun store(items: List<YieldSupplyMarketTokenDto>) {
persistenceStore.updateData { _ -> items } persistenceStore.updateData { _ -> items }
} }
} }

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.yieldsupply package com.tangem.datasource.local.yieldsupply
import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore { interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketToken>> fun get(): Flow<List<YieldSupplyMarketTokenDto>>
suspend fun getSyncOrNull(): List<YieldMarketToken>? suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>?
suspend fun store(items: List<YieldMarketToken>) suspend fun store(items: List<YieldSupplyMarketTokenDto>)
} }

View file

@ -0,0 +1,5 @@
package com.tangem.datasource.utils
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class SerializeNulls

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.utils
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
/**
* Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls].
*
[REDACTED_AUTHOR]
*/
internal object SerializeNullsFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = Types.getRawType(type)
if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) {
return null
}
val nextAdapter: JsonAdapter<Any> = moshi.nextAdapter(this, type, annotations)
return nextAdapter.serializeNulls()
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.utils
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
// --- DTO ---
@SerializeNulls
@JsonClass(generateAdapter = true)
data class UserWithNulls(val id: String?, val name: String?)
@JsonClass(generateAdapter = true)
data class UserWithoutNulls(val id: String?, val name: String?)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SerializeNullsFactoryTest {
private val moshi = Moshi.Builder()
.add(SerializeNullsFactory)
.build()
@Test
fun `should serialize nulls for annotated class`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = adapter.toJson(UserWithNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"id":null,"name":"John"}""")
}
@Test
fun `should skip nulls for non-annotated class`() {
val adapter = moshi.adapter(UserWithoutNulls::class.java)
val json = adapter.toJson(UserWithoutNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"name":"John"}""")
}
@Test
fun `should deserialize annotated class correctly`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = """{"id":null,"name":"Jane"}"""
val result = adapter.fromJson(json)
assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane"))
}
}

View file

@ -40,7 +40,7 @@ fun SimpleSettingsRow(
onItemsClick() onItemsClick()
} }
}, },
).testTag(BaseBottomSheetTestTags.ACTION_TITLE), ).testTag(BaseBottomSheetTestTags.ACTION_BUTTON),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -48,7 +48,8 @@ fun SimpleSettingsRow(
painter = painterResource(id = icon), painter = painterResource(id = icon),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(horizontal = if (redesign) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing20), .padding(horizontal = if (redesign) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing20)
.testTag(BaseBottomSheetTestTags.ACTION_ICON),
tint = rowColors.iconColor(enabled = enabled).value, tint = rowColors.iconColor(enabled = enabled).value,
) )
Column( Column(

View file

@ -1,12 +1,54 @@
package com.tangem.core.ui.components package com.tangem.core.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.LocalIsInDarkTheme
val LocalSystemBarsIconsController = staticCompositionLocalOf<SystemBarsIconsController> {
error("No SystemBarsIconsController provided")
}
class SystemBarsIconsController(private val systemUiController: SystemUiController) {
private var count by mutableIntStateOf(0)
fun setIcons(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean) {
if (count == 0) {
systemUiController.systemBarsDarkContentEnabled = darkIcons
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
}
count++
}
fun restoreIcons(isDarkTheme: Boolean) {
count--
if (count == 0) {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme
systemUiController.isNavigationBarContrastEnforced = false
}
}
}
@Composable
fun ProvideSystemBarsIconsController(content: @Composable () -> Unit) {
val systemUiController = rememberSystemUiController()
val controller = remember(systemUiController) { SystemBarsIconsController(systemUiController) }
CompositionLocalProvider(
LocalSystemBarsIconsController provides controller,
content = content,
)
}
/** /**
* Provides the ability to set a scrim for 3-button navigation * Provides the ability to set a scrim for 3-button navigation
* *
@ -43,19 +85,16 @@ fun NavigationBar3ButtonsScrim() {
*/ */
@Composable @Composable
fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) { fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) {
val systemUiController = rememberSystemUiController() val controller = LocalSystemBarsIconsController.current
val isDarkTheme = LocalIsInDarkTheme.current
SideEffect { SideEffect {
systemUiController.systemBarsDarkContentEnabled = darkIcons controller.setIcons(darkIcons, isNavigationBarContrastEnforced)
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
} }
val isDarkTheme = LocalIsInDarkTheme.current
DisposableEffect(isDarkTheme) { DisposableEffect(isDarkTheme) {
onDispose { onDispose {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme controller.restoreIcons(isDarkTheme)
systemUiController.isNavigationBarContrastEnforced = false
} }
} }
} }

View file

@ -51,7 +51,19 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
model.label?.let { Label(it) } when (val endContent = model.endContent) {
is BlockUM.EndContent.None -> Unit
is BlockUM.EndContent.Icon -> Icon(
painter = painterResource(id = endContent.resId),
contentDescription = null,
tint = when (endContent.accentType) {
BlockUM.AccentType.NONE -> TangemTheme.colors.text.primary1
BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent
BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning
},
)
is BlockUM.EndContent.Label -> Label(endContent.label)
}
} }
} }
} }

View file

@ -3,15 +3,29 @@ package com.tangem.core.ui.components.block.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
data class BlockUM( data class BlockUM(
val text: TextReference, val text: TextReference,
@DrawableRes val iconRes: Int, @DrawableRes val iconRes: Int,
val onClick: () -> Unit, val onClick: () -> Unit,
val accentType: AccentType = AccentType.NONE, val accentType: AccentType = AccentType.NONE,
val label: LabelUM? = null, val endContent: EndContent = EndContent.None,
) { ) {
@Immutable
sealed interface EndContent {
data object None : EndContent
data class Label(
val label: LabelUM,
) : EndContent
data class Icon(
val resId: Int,
val accentType: AccentType = AccentType.NONE,
) : EndContent
}
enum class AccentType { enum class AccentType {
NONE, ACCENT, WARNING, NONE, ACCENT, WARNING,
} }

View file

@ -27,15 +27,20 @@ private const val DISABLED_ICON_ALPHA = 0.4f
* [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4) * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4)
* */ * */
@Composable @Composable
fun BlockchainRow(model: BlockchainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit) { fun BlockchainRow(
model: BlockchainRowUM,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing8,
),
action: @Composable BoxScope.() -> Unit,
) {
RowContentContainer( RowContentContainer(
modifier = modifier modifier = modifier
.heightIn(min = TangemTheme.dimens.size52) .heightIn(min = TangemTheme.dimens.size52)
.padding( .padding(itemPadding),
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing8,
),
icon = { icon = {
RowIcon( RowIcon(
resId = model.iconResId, resId = model.iconResId,
@ -58,7 +63,7 @@ fun BlockchainRow(model: BlockchainRowUM, modifier: Modifier = Modifier, action:
} }
@Composable @Composable
private fun RowIcon( fun RowIcon(
@DrawableRes resId: Int, @DrawableRes resId: Int,
isColored: Boolean, isColored: Boolean,
showAccentBadge: Boolean, showAccentBadge: Boolean,

View file

@ -60,10 +60,10 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M
@Composable @Composable
fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
if (state.isExpanded) { if (state.isExpanded) {
ExpandedPortfolioHeader(state.state, modifier) ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier)
} else { } else {
TokenItem( TokenItem(
state = state.state, state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
modifier = modifier, modifier = modifier,
) )
@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
} }
@Composable @Composable
private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = Modifier) { private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
@ -130,11 +130,13 @@ private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier =
) )
} }
Icon( if (isCollapsable) {
modifier = Modifier.size(TangemTheme.dimens.size16), Icon(
painter = painterResource(id = R.drawable.ic_minimize_24), modifier = Modifier.size(TangemTheme.dimens.size16),
tint = TangemTheme.colors.icon.inactive, painter = painterResource(id = R.drawable.ic_minimize_24),
contentDescription = null, tint = TangemTheme.colors.icon.inactive,
) contentDescription = null,
)
}
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/** Tokens list item state */ /** Tokens list item state */
@Immutable @Immutable
@ -41,11 +42,12 @@ sealed interface TokensListItemUM {
} }
data class Portfolio( data class Portfolio(
val state: TokenItemState, val tokenItemUM: TokenItemState,
val isExpanded: Boolean, val isExpanded: Boolean,
val tokens: List<PortfolioTokensListItemUM>, val isCollapsable: Boolean,
val tokens: ImmutableList<PortfolioTokensListItemUM>,
) : TokensListItemUM { ) : TokensListItemUM {
override val id: String = state.id override val id: String = tokenItemUM.id
} }
data class Text(override val id: Any, val text: TextReference) : TokensListItemUM data class Text(override val id: Any, val text: TextReference) : TokensListItemUM

View file

@ -0,0 +1,26 @@
package com.tangem.core.ui.decompose
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Stable
interface ComposableListContentComponent<T> {
val uiState: StateFlow<T>
fun LazyListScope.content(uiState: T, modifier: Modifier)
companion object {
val EMPTY = EmptyComposableListContentComponent
}
}
object EmptyComposableListContentComponent : ComposableListContentComponent<Unit> {
override val uiState: StateFlow<Unit> = MutableStateFlow(Unit)
override fun LazyListScope.content(uiState: Unit, modifier: Modifier) { /* no-op */
}
}

View file

@ -90,12 +90,14 @@ fun getActiveIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22 "sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22 "apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "scroll", "scroll/test" -> R.drawable.img_scroll_22
"zklink", "zklink/test" -> R.drawable.img_zklink_22 "zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
"quai", "quai/test" -> R.drawable.img_quai_22 "quai", "quai/test" -> R.drawable.img_quai_22
"linea", "linea/test" -> R.drawable.img_linea_22
"arbitrum-nova" -> R.drawable.img_arbitrum_nova_22
else -> R.drawable.ic_alert_24 else -> R.drawable.ic_alert_24
} }
} }
@ -184,12 +186,14 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22 "sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22 "apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "scroll", "scroll/test" -> R.drawable.img_scroll_22
"zklink", "zklink/test" -> R.drawable.img_zklink_22 "zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
"quai", "quai/test" -> R.drawable.img_quai_22 "quai", "quai/test" -> R.drawable.img_quai_22
"linea", "linea/test" -> R.drawable.img_linea_22
"arbitrum-nova" -> R.drawable.img_arbitrum_nova_22
else -> R.drawable.ic_alert_24 else -> R.drawable.ic_alert_24
} }
} }
@ -281,12 +285,14 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22 "bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22
"sonic", "sonic/test" -> R.drawable.ic_sonic_22 "sonic", "sonic/test" -> R.drawable.ic_sonic_22
"apechain", "apechain/test" -> R.drawable.ic_apecoin_22 "apechain", "apechain/test" -> R.drawable.ic_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "scroll", "scroll/test" -> R.drawable.ic_scroll_22
"zklink", "zklink/test" -> R.drawable.ic_zklink_22 "zklink", "zklink/test" -> R.drawable.ic_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22 "vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22 "hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22
"quai", "quai/test" -> R.drawable.ic_quai_22 "quai", "quai/test" -> R.drawable.ic_quai_22
"linea", "linea/test" -> R.drawable.ic_linea_22
"arbitrum-nova" -> R.drawable.ic_arbitrum_nova_22
else -> R.drawable.ic_alert_24 else -> R.drawable.ic_alert_24
} }
} }

View file

@ -13,6 +13,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.LocalSystemBarsIconsController
import com.tangem.core.ui.components.SystemBarsIconsController
import com.tangem.core.ui.components.TangemShimmer import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.text.BladeAnimation import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation import com.tangem.core.ui.components.text.rememberBladeAnimation
@ -65,6 +67,8 @@ fun TangemTheme(
val themeColors = if (isDark) darkThemeColors() else lightThemeColors() val themeColors = if (isDark) darkThemeColors() else lightThemeColors()
val rememberedColors = remember { themeColors } val rememberedColors = remember { themeColors }
.also { it.update(themeColors) } .also { it.update(themeColors) }
val systemUiController = rememberSystemUiController()
val systemBarsIconsController = remember(systemUiController) { SystemBarsIconsController(systemUiController) }
val shapes = remember { TangemShapes(dimens) } val shapes = remember { TangemShapes(dimens) }
@ -103,6 +107,7 @@ fun TangemTheme(
LocalEventMessageHandler provides eventMessageHandler, LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize, LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(), LocalBladeAnimation provides rememberBladeAnimation(),
LocalSystemBarsIconsController provides systemBarsIconsController,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer, LocalTangemShimmer provides TangemShimmer,
@ -119,6 +124,14 @@ fun TangemTheme(
} }
} }
@Composable
fun ForceDarkTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalTangemColors provides darkThemeColors(),
content = content,
)
}
object TangemTheme { object TangemTheme {
val colors: TangemColors val colors: TangemColors
@Composable @Composable

View file

@ -1,7 +1,8 @@
package com.tangem.core.ui.test package com.tangem.core.ui.test
object BaseBottomSheetTestTags { object BaseBottomSheetTestTags {
const val ACTION_TITLE = "BASE_BOTTOM_SHEET_ACTION_TITLE" const val ACTION_BUTTON = "BASE_BOTTOM_SHEET_ACTION_BUTTON"
const val ACTION_ICON = "BASE_BOTTOM_SHEET_ACTION_ICON"
const val TITLE = "BASE_BOTTOM_SHEET_TITLE" const val TITLE = "BASE_BOTTOM_SHEET_TITLE"
const val SUBTITLE = "BASE_BOTTOM_SHEET_SUBTITLE" const val SUBTITLE = "BASE_BOTTOM_SHEET_SUBTITLE"
const val CLOSE_BUTTON = "BASE_BOTTOM_SHEET_CLOSE_BUTTON" const val CLOSE_BUTTON = "BASE_BOTTOM_SHEET_CLOSE_BUTTON"

View file

@ -4,13 +4,11 @@ object SendScreenTestTags {
const val SCREEN_CONTAINER = "SEND_SCREEN_CONTAINER" const val SCREEN_CONTAINER = "SEND_SCREEN_CONTAINER"
const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE" const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
const val AMOUNT_CONTAINER_TEXT = "SEND_SCREEN_AMOUNT_CONTAINER_TEXT"
const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD" const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD"
const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME"
const val PRIMARY_AMOUNT = "SEND_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT" const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT"
const val CURRENCY_BUTTON = "SEND_SCREEN_CURRENCY_BUTTON" const val MAX_BUTTON = "SEND_SCREEN_MAX_BUTTON"
const val FIAT_ICON = "SEND_SCREEN_FIAT_ICON"
const val CURRENCY_ICON = "SEND_SCREEN_CURRENCY_ICON"
const val MAX_BUTTON = "END_SCREEN_MAX_BUTTON"
const val PREVIOUS_BUTTON = "SEND_SCREEN_PREVIOUS_BUTTON" const val PREVIOUS_BUTTON = "SEND_SCREEN_PREVIOUS_BUTTON"
} }

View file

@ -9,6 +9,6 @@ object SwapTokenScreenTestTags {
const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK"
const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON"
const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN"
const val TOKEN_NAME = "SWAP_TOKEN_SCREEN_TOKEN_NAME" const val TOKEN_SYMBOL = "SWAP_TOKEN_SCREEN_TOKEN_SYMBOL"
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
} }

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object TokenReceiveQrCodeBottomSheetTestTags {
const val TITLE = "TOKEN_RECEIVE_QR_CODE_BOTTOM_SHEET_TITLE"
const val QR_CODE = "TOKEN_RECEIVE_QR_CODE_BOTTOM_SHEET_QR_CODE"
const val ADDRESS = "TOKEN_RECEIVE_QR_CODE_BOTTOM_SHEET_ADDRESS"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object TokenReceiveWarningBottomSheetTestTags {
const val BOTTOM_SHEET = "TOKEN_RECEIVE_WARNING_BOTTOM_SHEET"
}

View file

@ -7,4 +7,5 @@ object WalletConnectScreenTestTags {
const val APP_NAME = "WALLET_CONNECT_SCREEN_APP_NAME" const val APP_NAME = "WALLET_CONNECT_SCREEN_APP_NAME"
const val APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON" const val APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON"
const val APP_URL = "WALLET_CONNECT_SCREEN_APP_URL" const val APP_URL = "WALLET_CONNECT_SCREEN_APP_URL"
const val WALLET_CONNECT_IMAGE = "WALLET_CONNECT_SCREEN_WALLET_CONNECT_IMAGE"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M11.413,2.426C12.757,2.426 13.43,2.426 13.943,2.688C14.395,2.918 14.762,3.285 14.992,3.736C15.254,4.25 15.253,4.922 15.253,6.266V9.736C15.253,11.079 15.254,11.752 14.992,12.265C14.762,12.716 14.395,13.084 13.943,13.314C13.43,13.575 12.757,13.575 11.413,13.575H4.587C3.243,13.575 2.57,13.575 2.057,13.314C1.605,13.084 1.238,12.716 1.008,12.265C0.746,11.752 0.746,11.079 0.746,9.736V6.266C0.746,4.922 0.746,4.25 1.008,3.736C1.238,3.285 1.605,2.918 2.057,2.688C2.57,2.426 3.243,2.426 4.587,2.426H11.413ZM8,5.063C7.876,5.063 7.814,5.063 7.763,5.073C7.551,5.115 7.386,5.281 7.344,5.492C7.334,5.544 7.333,5.606 7.333,5.729V7.334H5.729C5.606,7.334 5.544,7.334 5.492,7.344C5.281,7.386 5.115,7.551 5.073,7.763C5.063,7.815 5.063,7.877 5.063,8.001C5.063,8.124 5.063,8.186 5.073,8.238C5.115,8.449 5.281,8.615 5.492,8.657C5.544,8.667 5.606,8.667 5.729,8.667H7.333V10.271C7.333,10.394 7.334,10.456 7.344,10.508C7.386,10.719 7.551,10.885 7.763,10.927C7.814,10.937 7.876,10.938 8,10.938C8.124,10.938 8.186,10.937 8.238,10.927C8.449,10.885 8.615,10.719 8.657,10.508C8.667,10.456 8.667,10.394 8.667,10.271V8.667H10.27C10.394,8.667 10.456,8.667 10.508,8.657C10.719,8.615 10.885,8.449 10.927,8.238C10.937,8.186 10.937,8.124 10.937,8.001C10.937,7.877 10.937,7.815 10.927,7.763C10.885,7.551 10.719,7.386 10.508,7.344C10.456,7.333 10.394,7.334 10.27,7.334H8.667V5.729C8.667,5.606 8.667,5.544 8.657,5.492C8.615,5.281 8.449,5.115 8.238,5.073C8.186,5.063 8.124,5.063 8,5.063Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M8.857,6H7.575C7.486,6 7.407,6.058 7.377,6.146L4.006,15.851C3.981,15.924 4.032,16 4.105,16H5.388C5.476,16 5.555,15.942 5.586,15.855L8.956,6.149C8.981,6.076 8.93,6 8.857,6ZM10.416,10.067C10.383,9.97 10.252,9.97 10.218,10.067L9.554,11.981C9.537,12.03 9.537,12.084 9.554,12.133L10.846,15.854C10.876,15.942 10.955,16 11.044,16H12.326C12.399,16 12.45,15.924 12.425,15.851L10.416,10.067ZM13.685,11.941C13.719,12.038 13.849,12.038 13.883,11.941L15.894,6.149C15.919,6.076 15.868,6 15.795,6H14.513C14.424,6 14.345,6.058 14.315,6.146L13.02,9.874C13.003,9.923 13.003,9.977 13.02,10.026L13.685,11.941ZM11.157,6.144C11.127,6.058 11.048,6 10.96,6H9.674C9.585,6 9.506,6.058 9.476,6.145L6.106,15.851C6.081,15.924 6.132,16 6.205,16H7.487C7.576,16 7.655,15.942 7.685,15.855L10.265,8.423C10.282,8.374 10.348,8.374 10.365,8.423L12.945,15.855C12.976,15.942 13.055,16 13.143,16H14.425C14.499,16 14.55,15.924 14.524,15.851L11.157,6.144ZM17.895,6H16.612C16.524,6 16.445,6.058 16.414,6.145L14.07,12.898C14.053,12.946 14.053,13 14.07,13.049L14.735,14.964C14.768,15.061 14.899,15.061 14.933,14.964L17.253,8.28L17.993,6.149C18.019,6.076 17.968,6 17.895,6Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="24dp"
android:viewportWidth="19"
android:viewportHeight="24">
<path
android:pathData="M12.632,12.113C12.632,12.331 12.552,12.511 12.382,12.682L8.287,16.687C8.148,16.825 7.984,16.889 7.787,16.889C7.388,16.889 7.063,16.575 7.063,16.176C7.063,15.979 7.149,15.798 7.292,15.655L10.93,12.118L7.292,8.57C7.149,8.427 7.063,8.251 7.063,8.049C7.063,7.65 7.388,7.331 7.787,7.331C7.984,7.331 8.148,7.4 8.287,7.538L12.382,11.543C12.558,11.714 12.632,11.894 12.632,12.113Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="16dp"
android:viewportWidth="17"
android:viewportHeight="16">
<path
android:pathData="M10.49,1.932C10.866,2.174 11.072,2.631 10.988,3.121L10.323,7.004C10.312,7.067 10.355,7.127 10.418,7.138L12.198,7.443C12.706,7.53 12.997,7.933 13.079,8.318C13.162,8.702 13.063,9.161 12.713,9.481L7.814,13.946C7.422,14.304 6.898,14.317 6.51,14.067C6.135,13.825 5.928,13.368 6.012,12.878L6.677,8.995C6.688,8.932 6.646,8.872 6.582,8.861L4.803,8.556C4.295,8.469 4.003,8.066 3.921,7.681C3.839,7.297 3.937,6.838 4.288,6.519L9.186,2.053C9.579,1.695 10.102,1.682 10.49,1.932Z"
android:fillColor="#0099FF"/>
</vector>

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