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.createWalletSelection.api)
implementation(projects.features.createWalletSelection.impl)
implementation(projects.features.createWalletStart.api)
implementation(projects.features.createWalletStart.impl)
implementation(projects.features.home.api)
implementation(projects.features.home.impl)
implementation(projects.features.account.api)

View file

@ -1,8 +1,8 @@
package com.tangem.common
import android.Manifest
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario
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.
*
* @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.
* Use for accessing inner elements of compound components.
* Default: false (merged tree - accessibility view).
@ -129,11 +131,13 @@ abstract class BaseTestCase : TestCase(
* Default: Int.MAX_VALUE (unlimited depth).
*/
fun printSemanticTree(
rootIndex: Int = 0,
useUnmergedTree: Boolean = false,
tag: String = "SEMANTIC_TREE",
maxDepth: Int = Int.MAX_VALUE)
{
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth)
maxDepth: Int = Int.MAX_VALUE
) {
composeTestRule.onAllNodes(isRoot(), useUnmergedTree = useUnmergedTree)[rootIndex]
.printToLog(tag, maxDepth)
}
fun waitForIdle() = composeTestRule.waitForIdle()

View file

@ -4,6 +4,7 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
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") {
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
fun BaseTestCase.checkWalletConnectBottomSheet() {
waitForIdle()
step("Assert 'Wallet Connect' bottom sheet title is displayed") {
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") {
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") {
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) {
waitForIdle()
step("Assert connection details title is displayed") {
onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() }
}
@ -128,7 +159,7 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() }
}
step("Assert connected dApp name: '$dAppName'") {
onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) }
onWalletConnectDetailsBottomSheet { appName.assertTextContains(dAppName) }
}
step("Assert connected network symbol is displayed") {
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 androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.ui.R as CoreUiR
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
@ -200,6 +201,10 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
val snackbarCopiedAddressMessage: KNode = child {
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
}
/**
* 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 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
class BaseBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BaseBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val hideButton: KNode = child {
hasText(getResourceString(R.string.token_details_hide_token))
hasTestTag(BaseBottomSheetTestTags.ACTION_TITLE)
val showQrCodeButton: KNode = child {
hasText(getResourceString(R.string.token_receive_show_qr_code_title))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onBottomSheet(function: BaseBottomSheetPageObject.() -> Unit) =
internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
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.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasTestTag as withTestTag
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
@ -19,6 +18,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(SendScreenTestTags.SCREEN_CONTAINER)
}
val closeButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
@ -29,13 +33,18 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val amountContainerText: KNode = child {
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT)
val amountInputTextField: KNode = child {
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val amountInputTextField: KNode = child {
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
val tokenName: KNode = child {
hasTestTag(SendScreenTestTags.TOKEN_NAME)
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(SendScreenTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
@ -44,28 +53,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
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 {
hasTestTag(SendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true
}
val previousButton: KNode = child {
hasTestTag(SendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_next))

View file

@ -71,6 +71,10 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(R.string.common_swap))
}
fun tokenSymbol(symbol: String): KNode = child {
hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL)
}
}
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.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect))
hasTestTag(WalletConnectBottomSheetTestTags.TITLE)
useUnmergedTree = true
}

View file

@ -53,6 +53,21 @@ class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
hasText(getResourceString(R.string.wc_new_connection))
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) =

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
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.scenarios.checkMultiCurrencyMainScreen
import com.tangem.scenarios.checkSingleCurrencyMainScreen
@ -15,6 +19,10 @@ import org.junit.Test
@HiltAndroidTest
class ScanCardTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD),
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("868")
@DisplayName("Scan: Scanning single-currency cards")
@Test

View file

@ -7,9 +7,8 @@ import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -117,116 +116,20 @@ class StakingTest : BaseTestCase() {
step("Click on 'Staking block'") {
onTokenDetailsScreen { stakingBlock.clickWithAssertion() }
}
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 '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("Check 'Staking details' screen") {
checkStakingDetailsScreen(withStaking = true)
}
step("Click 'Stake more' button") {
onStakingDetailsScreen { stakeMoreButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onSendScreen { screenContainer.assertIsDisplayed() }
}
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("Check 'Staking' screen") {
checkStakingScreen(stakingAmount)
}
step("Click on 'Next' button") {
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' 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() }
step("Check 'Staking confirm' screen") {
checkStakingConfirmScreen()
}
}
}
@ -284,107 +187,20 @@ class StakingTest : BaseTestCase() {
step("Click on 'Stake' button") {
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
}
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
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("Check 'Staking details' screen") {
checkStakingDetailsScreen(withStaking = false)
}
step("Click 'Stake' button") {
onStakingDetailsScreen { stakeButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onSendScreen { screenContainer.assertIsDisplayed() }
}
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("Check 'Staking' screen") {
checkStakingScreen(stakingAmount)
}
step("Click on 'Next' button") {
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' 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() }
step("Check 'Staking confirm' screen") {
checkStakingConfirmScreen()
}
}
}

View file

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

View file

@ -67,7 +67,11 @@ internal class DefaultTangemPayStorage @Inject constructor(
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(createOrderIdKey(customerWalletAddress))
}

View file

@ -49,8 +49,12 @@ internal object AccountDomainModule {
@Singleton
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
)
}
@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.WalletsRepository
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.YieldSupplyApyUpdateUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
@ -454,21 +454,17 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun provideYieldSupplyApyFlowUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyApyFlowUseCase {
fun provideYieldSupplyApyFlowUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyFlowUseCase {
return YieldSupplyApyFlowUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository,
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyApyUpdateUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyApyUpdateUseCase {
fun provideYieldSupplyApyUpdateUseCase(yieldSupplyRepository: YieldSupplyRepository): 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.error.FeeErrorResolver
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.usecase.*
import dagger.Module
@ -82,30 +82,54 @@ internal object YieldSupplyDomainModule {
@Provides
@Singleton
fun provideYieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyGetTokenStatusUseCase {
return YieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository,
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetApyUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyGetApyUseCase {
fun provideYieldSupplyGetApyUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetApyUseCase {
return YieldSupplyGetApyUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository,
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetChartUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyGetChartUseCase {
fun provideYieldSupplyGetChartUseCase(yieldSupplyRepository: YieldSupplyRepository): 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
Hyperliquid, HyperliquidTestnet -> 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.core.decompose.context.AppComponentContext
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent
@ -12,6 +13,7 @@ import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.createwalletstart.CreateWalletStartComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
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.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
@ -97,6 +100,7 @@ internal class ChildFactory @Inject constructor(
private val usedeskComponentFactory: UsedeskComponent.Factory,
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -140,9 +144,15 @@ internal class ChildFactory @Inject constructor(
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(
context = context,
params = ManageTokensComponent.Params(route.userWalletId, source),
params = ManageTokensComponent.Params(mode, source),
componentFactory = manageTokensComponentFactory,
)
}
@ -200,7 +210,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild(
context = context,
params = OnrampComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param,
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
shouldLaunchSepa = route.shouldLaunchSepa,
@ -274,7 +284,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild(
context = context,
params = TokenDetailsComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param
userWalletId = route.userWalletId,
currency = route.currency,
),
componentFactory = tokenDetailsComponentFactory,
@ -284,7 +294,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild(
context = context,
params = StakingComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param,
userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId,
yieldId = route.yieldId,
),
@ -297,7 +307,7 @@ internal class ChildFactory @Inject constructor(
params = SwapComponent.Params(
currencyFrom = route.currencyFrom,
currencyTo = route.currencyTo,
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param,
userWalletId = route.userWalletId,
isInitialReverseOrder = route.isInitialReverseOrder,
screenSource = route.screenSource,
),
@ -308,7 +318,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild(
context = context,
params = SendComponent.Params(
userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param,
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
@ -468,6 +478,19 @@ internal class ChildFactory @Inject constructor(
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 -> {
createComponentChild(
context = context,

View file

@ -51,50 +51,25 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class CurrencyDetails(
val portfolioId: PortfolioId,
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/currency_details/${portfolioId.stringValue}/${currency.id.value}") {
companion object {
operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) = CurrencyDetails(
portfolioId = PortfolioId(userWalletId),
currency = currency,
)
}
}
) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}")
@Serializable
data class Send(
val portfolioId: PortfolioId,
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
val transactionId: String? = null,
val amount: String? = null,
val tag: String? = null,
val destinationAddress: String? = null,
) : AppRoute(
path = "/send/${portfolioId.stringValue}/${currency.id.value}?" +
path = "/send/${userWalletId.stringValue}/${currency.id.value}?" +
"&$transactionId" +
"&$amount" +
"&$tag" +
"&$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
data class Details(
@ -147,8 +122,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val source: Source,
val userWalletId: UserWalletId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId") {
val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
enum class Source {
STORIES,
@ -199,51 +174,26 @@ sealed class AppRoute(val path: String) : Route {
data class Swap(
val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null,
val portfolioId: PortfolioId,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
val screenSource: String,
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${portfolioId.stringValue}" +
"/${userWalletId.stringValue}" +
"/$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
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")
@Serializable
data class Staking(
val portfolioId: PortfolioId,
val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID,
val yieldId: String,
) : AppRoute(path = "/staking/${portfolioId.stringValue}/${cryptoCurrencyId.value}/$yieldId") {
companion object {
operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yieldId: String) =
Staking(
portfolioId = PortfolioId(userWalletId),
cryptoCurrencyId = cryptoCurrencyId,
yieldId = yieldId,
)
}
}
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId")
@Serializable
data class PushNotification(
@ -287,25 +237,11 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Onramp(
val source: OnrampSource,
val portfolioId: PortfolioId,
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
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())
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
@ -375,6 +311,16 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
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
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,
onClick = { onClick(value.accountId) },
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.preview.AmountScreenClickIntentsStub
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.buttons
import com.tangem.core.ui.res.TangemThemePreview
/**
* Amount screen with field
* @param amountState amount state
* @param isBalanceHidden flag hidden balances
* @param clickIntents amount screen clicks
*/
@Composable
fun AmountScreenContent(
amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: AmountScreenClickIntents,
modifier: Modifier = Modifier,
extraContent: (@Composable () -> Unit)? = null,
@ -38,32 +34,17 @@ fun AmountScreenContent(
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (amountState.isRedesignEnabled) {
amountFieldV2(
amountState = amountState,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
onCurrencyChange = clickIntents::onCurrencyChangeClick,
onMaxAmountClick = clickIntents::onMaxValueClick,
)
if (extraContent != null) {
item("EXTRA_CONTENT_KEY") {
extraContent()
}
amountFieldV2(
amountState = amountState,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
onCurrencyChange = clickIntents::onCurrencyChangeClick,
onMaxAmountClick = clickIntents::onMaxValueClick,
)
if (extraContent != null) {
item("EXTRA_CONTENT_KEY") {
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 {
AmountScreenContent(
amountState = amountState,
isBalanceHidden = false,
clickIntents = AmountScreenClickIntentsStub,
)
}

View file

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

View file

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

View file

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

View file

@ -1,104 +1,35 @@
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.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.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
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.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DOT
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]
*
* @property clickIntents amount screen clicks
* @property appCurrency selected app currency
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status
* @property maxEnterAmount max enter amount data
* @property iconStateConverter currency icon converter
* @property isBalanceHidden is balance hidden status
*/
@Suppress("LongParameterList")
class AmountStateConverterV2(
class AmountStateConverter(
private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -108,7 +39,7 @@ class AmountStateConverterV2(
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverterV2(
AmountFieldConverter(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
@ -118,19 +49,13 @@ class AmountStateConverterV2(
override fun convert(value: AmountParameters): AmountState {
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
return AmountState.Empty(isRedesignEnabled = true)
return AmountState.Empty
}
return AmountState.Data(
title = value.title,
availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
).orMaskWithStars(isBalanceHidden),
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) {
TextReference.EMPTY
@ -145,25 +70,6 @@ class AmountStateConverterV2(
amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false,
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.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.fiat
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 crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
return prevState.copy(
availableBalance = availableBalance.orMaskWithStars(isBalanceHidden),
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) {
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.AmountType
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
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]
*
@ -89,7 +24,7 @@ class AmountFieldConverter(
* @property appCurrency selected app currency
* @property cryptoCurrencyStatus current cryptocurrency status
*/
class AmountFieldConverterV2(
class AmountFieldConverter(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,

View file

@ -54,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer(
isError = isLessThanMinimumIfProvided,
error = when {
isLessThanMinimumIfProvided -> {
val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) }
val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
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.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
/** Model for amount state */
@ -12,18 +11,13 @@ import java.math.BigDecimal
sealed class AmountState {
abstract val isPrimaryButtonEnabled: Boolean
abstract val isRedesignEnabled: Boolean
/**
* @param isPrimaryButtonEnabled indicates if next state button enabled
* @param title title
* @param availableBalance user crypto currency balance with fiat balance
* @param availableBalanceCrypto user crypto currency balance in crypto
* @param availableBalanceFiat user crypto currency balance in fiat
* @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 appCurrency app currency
* @param isEditingDisabled indicated whether amount is editable
@ -32,17 +26,11 @@ sealed class AmountState {
*/
data class Data(
override val isPrimaryButtonEnabled: Boolean,
override val isRedesignEnabled: Boolean,
val title: TextReference,
@Deprecated("Remove with SEND_REDESIGNED toggle")
val availableBalance: TextReference,
val availableBalanceCrypto: TextReference,
val availableBalanceFiat: TextReference,
val tokenName: TextReference,
val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel,
val appCurrency: AppCurrency,
val isEditingDisabled: Boolean = false,
@ -50,8 +38,7 @@ sealed class AmountState {
val isIgnoreReduce: Boolean = false,
) : AmountState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
override val isRedesignEnabled: Boolean,
) : AmountState()
data object Empty : AmountState() {
override val isPrimaryButtonEnabled: Boolean = false
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.common.ui.amountScreen.preview
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.core.ui.components.currency.icon.CurrencyIconState
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.AmountType
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
object AmountStatePreviewData {
val emptyState = AmountState.Empty(isRedesignEnabled = true)
val emptyState = AmountState.Empty
val amountState = AmountState.Data(
isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"),
availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
availableBalanceFiat = stringReference("1 232 129,12 \$"),
tokenIconState = CurrencyIconState.Loading,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference("USDT"),
iconState = CurrencyIconState.Locked,
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference("USD"),
isFiat = true,
),
),
appCurrency = AppCurrency.Default,
tokenName = stringReference("Tether"),
amountTextField = AmountFieldModel(
@ -65,12 +51,9 @@ object AmountStatePreviewData {
isValuePasted = false,
onValuePastedTriggerDismiss = {},
),
isSegmentedButtonsEnabled = true,
selectedButton = 0,
isRedesignEnabled = false,
)
val amountWithValueState = amountState.copy(
private val amountWithValueState = amountState.copy(
amountTextField = amountState.amountTextField.copy(
value = "100.00",
cryptoAmount = amountState.amountTextField.cryptoAmount.copy(
@ -84,16 +67,10 @@ object AmountStatePreviewData {
)
val amountStateV2 = amountState.copy(
isRedesignEnabled = true,
availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"),
)
val amountWithValueFiatState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
)
val amountStateV2WithoutRates = amountState.copy(
amountTextField = amountState.amountTextField.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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
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.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.res.TangemThemePreview
import com.tangem.core.ui.test.BaseAmountBlockTestTags
@Composable
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)
.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(
text = firstAmount,
style = TangemTheme.typography.h2,
@ -68,8 +78,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24)
.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
.padding(top = TangemTheme.dimens.spacing24),
)
Text(
text = secondAmount,
@ -78,8 +87,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8)
.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
.padding(top = TangemTheme.dimens.spacing8),
)
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseAmountBlockTestTags
@Composable
fun AmountBlockV2(
@ -133,6 +135,7 @@ private fun AmountBlockV2(
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -142,6 +145,7 @@ private fun AmountBlockV2(
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
)
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.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
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.currency.icon.CurrencyIcon
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.stringResourceSafe
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"
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(
amountState: AmountState,
modifier: Modifier = Modifier,
@ -118,6 +62,7 @@ internal fun LazyListScope.amountFieldV2(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
}
AmountFieldV2(
@ -175,7 +120,8 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
indication = ripple(),
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
private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = amountUM !is AmountState.Data,
targetState = amountUM,
modifier = modifier,
) { isContent ->
if (isContent) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
TextShimmer(
style = TangemTheme.typography.subtitle2,
modifier = Modifier.width(56.dp),
)
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(72.dp),
)
}
} 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),
) { currentAmount ->
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
when (currentAmount) {
is AmountState.Data -> {
val amountUM = amountUM as AmountState.Data
Text(
text = amountUM.tokenName.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
modifier = Modifier.testTag(SendScreenTestTags.TOKEN_NAME),
)
EllipsisText(
text = amountUM.availableBalanceFiat.resolveReference(),
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)
.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,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.fiatAmount.currencySymbol.length),
modifier = Modifier.width(72.dp),
)
}
}

View file

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

View file

@ -8,7 +8,6 @@ sealed class NavigationButtonsState {
data class Data(
val primaryButton: NavigationButton?,
val prevButton: NavigationButton?,
val extraButtons: Pair<NavigationButton, NavigationButton>?,
val txUrl: String? = null,
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.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
internal object NavigationButtonsPreview {
@ -26,16 +25,6 @@ internal object NavigationButtonsPreview {
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(
textReference = resourceReference(R.string.common_close),
isSecondary = false,
@ -47,7 +36,6 @@ internal object NavigationButtonsPreview {
val allButtons = NavigationButtonsState.Data(
primaryButton = finished,
prevButton = prev,
extraButtons = extraButtons,
txUrl = "https://tangem.com",
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.utils.PriceChangeConverter
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.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
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.models.StatusSource
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.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -35,10 +39,13 @@ import java.math.BigDecimal
*/
class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val apyMap: Map<String, String> = emptyMap(),
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
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? = {
createSubtitleState(it, appCurrency)
},
@ -144,7 +151,10 @@ class TokenItemStateConverter(
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
?.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) {
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
@ -158,14 +168,33 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
val earnApyText = resolveEarnApy(currencyStatus, apyMap)?.let { apy ->
resourceReference(
R.string.yield_module_earn_badge,
wrappedList(apy),
)
}
TokenItemState.TitleState.Content(
text = stringReference(currencyStatus.currency.name),
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(
currencyStatus: CryptoCurrencyStatus,
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.block.BlockCard
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.extensions.*
import com.tangem.core.ui.res.TangemTheme
@ -76,8 +73,6 @@ fun UserWalletItem(
balance = state.balance,
)
state.label?.let { Label(it) }
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> Unit
UserWalletItemUM.EndIcon.Arrow -> {
@ -94,6 +89,13 @@ fun UserWalletItem(
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"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked,
label = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
endIcon = UserWalletItemUM.EndIcon.Warning,
isEnabled = true,
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.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.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
@ -52,7 +49,6 @@ class UserWalletItemUMConverter(
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork,
label = getLabelOrNull(userWallet = this),
)
}
}
@ -61,17 +57,6 @@ class UserWalletItemUMConverter(
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 {
val text = when (userWallet) {
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.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.wallet.UserWalletId
import javax.annotation.concurrent.Immutable
@ -17,12 +16,13 @@ data class UserWalletItemUM(
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
val label: LabelUM? = null,
) {
enum class EndIcon {
None,
Arrow,
Checkmark,
Warning,
}
sealed class Balance {

View file

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

View file

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

View file

@ -156,7 +156,7 @@ interface TangemTechApi {
@Path("walletId") walletId: String,
@Header("If-Match") eTag: String,
@Body body: SaveWalletAccountsResponse,
): ApiResponse<Unit>
): ApiResponse<GetWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived")
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.YieldModuleStatusResponse
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 retrofit2.http.Body
import retrofit2.http.GET
@ -15,13 +15,13 @@ import retrofit2.http.Query
interface YieldSupplyApi {
@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}")
suspend fun getYieldTokenStatus(
@Path("chainId") chainId: Int,
@Path("tokenAddress") tokenAddress: String,
): ApiResponse<YieldTokenStatusResponse>
): ApiResponse<YieldSupplyMarketTokenDto>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart")
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.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldMarketsResponse(
@Json(name = "tokens") val marketDtos: List<MarketDto>,
@Json(name = "tokens") val marketDtos: List<YieldSupplyMarketTokenDto>,
@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
@JsonClass(generateAdapter = true)
data class YieldTokenStatusResponse(
data class YieldSupplyMarketTokenDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: 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.JsonClass
import com.tangem.datasource.utils.SerializeNulls
@JsonClass(generateAdapter = true)
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.local.config.providers.models.ProviderModel
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.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -28,6 +29,7 @@ class MoshiModule {
@NetworkMoshi
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(SerializeNullsFactory)
.add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public")

View file

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

View file

@ -12,5 +12,7 @@ interface TangemPayStorage {
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
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.firstOrNull
internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketToken>>,
private val persistenceStore: DataStore<List<YieldSupplyMarketTokenDto>>,
) : 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()
}
override suspend fun store(items: List<YieldMarketToken>) {
override suspend fun store(items: List<YieldSupplyMarketTokenDto>) {
persistenceStore.updateData { _ -> items }
}
}

View file

@ -1,13 +1,13 @@
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
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()
}
},
).testTag(BaseBottomSheetTestTags.ACTION_TITLE),
).testTag(BaseBottomSheetTestTags.ACTION_BUTTON),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -48,7 +48,8 @@ fun SimpleSettingsRow(
painter = painterResource(id = icon),
contentDescription = null,
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,
)
Column(

View file

@ -1,12 +1,54 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.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
*
@ -43,19 +85,16 @@ fun NavigationBar3ButtonsScrim() {
*/
@Composable
fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) {
val systemUiController = rememberSystemUiController()
val controller = LocalSystemBarsIconsController.current
val isDarkTheme = LocalIsInDarkTheme.current
SideEffect {
systemUiController.systemBarsDarkContentEnabled = darkIcons
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
controller.setIcons(darkIcons, isNavigationBarContrastEnforced)
}
val isDarkTheme = LocalIsInDarkTheme.current
DisposableEffect(isDarkTheme) {
onDispose {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme
systemUiController.isNavigationBarContrastEnforced = false
controller.restoreIcons(isDarkTheme)
}
}
}

View file

@ -51,7 +51,19 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
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 com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
data class BlockUM(
val text: TextReference,
@DrawableRes val iconRes: Int,
val onClick: () -> Unit,
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 {
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)
* */
@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(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size52)
.padding(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing8,
),
.padding(itemPadding),
icon = {
RowIcon(
resId = model.iconResId,
@ -58,7 +63,7 @@ fun BlockchainRow(model: BlockchainRowUM, modifier: Modifier = Modifier, action:
}
@Composable
private fun RowIcon(
fun RowIcon(
@DrawableRes resId: Int,
isColored: Boolean,
showAccentBadge: Boolean,

View file

@ -60,10 +60,10 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M
@Composable
fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
if (state.isExpanded) {
ExpandedPortfolioHeader(state.state, modifier)
ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier)
} else {
TokenItem(
state = state.state,
state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
}
@Composable
private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = Modifier) {
private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
@ -130,11 +130,13 @@ private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier =
)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_minimize_24),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
if (isCollapsable) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_minimize_24),
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.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/** Tokens list item state */
@Immutable
@ -41,11 +42,12 @@ sealed interface TokensListItemUM {
}
data class Portfolio(
val state: TokenItemState,
val tokenItemUM: TokenItemState,
val isExpanded: Boolean,
val tokens: List<PortfolioTokensListItemUM>,
val isCollapsable: Boolean,
val tokens: ImmutableList<PortfolioTokensListItemUM>,
) : TokensListItemUM {
override val id: String = state.id
override val id: String = tokenItemUM.id
}
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
"sonic", "sonic/test" -> R.drawable.img_sonic_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
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_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
}
}
@ -184,12 +186,14 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_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
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_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
}
}
@ -281,12 +285,14 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22
"sonic", "sonic/test" -> R.drawable.ic_sonic_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
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_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
}
}

View file

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

View file

@ -1,7 +1,8 @@
package com.tangem.core.ui.test
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 SUBTITLE = "BASE_BOTTOM_SHEET_SUBTITLE"
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 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 TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME"
const val PRIMARY_AMOUNT = "SEND_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT"
const val CURRENCY_BUTTON = "SEND_SCREEN_CURRENCY_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 MAX_BUTTON = "SEND_SCREEN_MAX_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 SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON"
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"
}

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 APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON"
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