Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 14:25:11 +03:00
commit 5ee5b4f61b
563 changed files with 13917 additions and 4692 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()
@ -141,7 +145,7 @@ abstract class BaseTestCase : TestCase(
private fun setFeatureToggles() {
runBlocking {
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true)
changeToggle("NEW_TOKEN_RECEIVE_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)

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

@ -1,5 +1,6 @@
package com.tangem.scenarios
import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
@ -7,6 +8,7 @@ import com.tangem.screens.*
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.utils.StringsSigns.DASH_SIGN
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.scanCard(
@ -65,12 +67,29 @@ fun BaseTestCase.openMainScreen(
}
}
fun BaseTestCase.synchronizeAddresses(balance: String) {
fun BaseTestCase.synchronizeAddresses(
balance: String? = null,
isBalanceAvailable: Boolean = true
) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { totalBalanceText.assertTextContains(balance) }
when {
!isBalanceAvailable -> step("Assert wallet balance = '$DASH_SIGN'") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
}
balance != null -> {
step("Assert wallet balance != '$DASH_SIGN'") {
onMainScreen { totalBalanceText.assert(!hasText(DASH_SIGN)) }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { totalBalanceText.assertTextContains(balance) }
}
}
else -> step("Assert wallet balance != '$DASH_SIGN'") {
onMainScreen { totalBalanceText.assert(!hasText(DASH_SIGN)) }
}
}
}
@ -85,4 +104,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

@ -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

@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.test.SendConfirmScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
@ -34,6 +35,11 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true
}
val sendingText: KNode = child {
hasTestTag(SendConfirmScreenTestTags.SENDING_TEXT)
useUnmergedTree = true
}
fun minimumSendAmountErrorIcon(amount: String): KNode = child {
hasAnySibling(
withText(

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,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 ThirdPartyAppPageObject : KScreen<ThirdPartyAppPageObject>() {
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

@ -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

@ -9,10 +9,6 @@ import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -26,7 +22,6 @@ class BlockchainTest : BaseTestCase() {
@Test
fun adaCheckMinAmountTest() {
val tokenName = "Cardano"
val balance = "$0.00"
val errorSendAmount = "0.1"
val validSendAmount = "10"
val minAmount = "ADA 1.00"
@ -46,7 +41,7 @@ class BlockchainTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
@ -121,7 +116,6 @@ class BlockchainTest : BaseTestCase() {
val tokenName = "XRP Ledger"
val amount = "1.00"
val currencySymbol = "XRP"
val balance = "$0.00"
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val rippleAccountInfoScenarioName = "ripple_account_info"
@ -151,7 +145,7 @@ class BlockchainTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }

View file

@ -1,7 +1,6 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
@ -22,7 +21,6 @@ class BuyTokenTest : BaseTestCase() {
fun errorInProvidersLoadingTest() {
val scenarioName = "payment_methods"
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
setupHooks(
additionalAfterSection = {
@ -39,7 +37,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -68,7 +66,6 @@ class BuyTokenTest : BaseTestCase() {
fun validateCurrencySelectorTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val popularFiatsTitle = "Popular Fiats"
val otherCurrenciesTitle = "Other currencies"
val australianDollar = "AUD"
@ -84,7 +81,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -146,7 +143,6 @@ class BuyTokenTest : BaseTestCase() {
fun validateBuyTokenScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val euro = "EUR"
val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL"
@ -160,7 +156,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -237,7 +233,6 @@ class BuyTokenTest : BaseTestCase() {
fun validateResidenceSettingsScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val country = "Albania"
val unavailableCountry = "Lebanon"
val scenarioName = "payment_methods"
@ -250,7 +245,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -318,7 +313,6 @@ class BuyTokenTest : BaseTestCase() {
fun validateProvidersScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val paymentMethod = "Invoice Revolut Pay"
val fiatAmount = "1"
val providerNameMercuryo = "Mercuryo"
@ -333,7 +327,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -407,7 +401,6 @@ class BuyTokenTest : BaseTestCase() {
fun validatePaymentMethodScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val card = "Card"
val googlePay = "Google Pay"
val invoiceRevolutPay = "Invoice Revolut Pay"
@ -424,7 +417,7 @@ class BuyTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }

View file

@ -3,7 +3,6 @@ package com.tangem.tests
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickWithAssertion
@ -28,6 +27,8 @@ class FeedbackTest : BaseTestCase() {
@DisplayName("Send feedback: from details")
@Test
fun sendFeedbackFromDetailsTest() {
val gmailText = "Welcome to Gmail"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
@ -42,8 +43,8 @@ class FeedbackTest : BaseTestCase() {
step("Click 'Contact support' button") {
onDetailsScreen { contactSupportButton.clickWithAssertion() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
}
}
}
@ -52,10 +53,10 @@ class FeedbackTest : BaseTestCase() {
@DisplayName("Send feedback: failed transaction")
@Test
fun sendFeedbackFromFailedTransactionTest() {
val balance = TOTAL_BALANCE
val tokenName = "Polygon"
val recipientAddress = RECIPIENT_ADDRESS
val sendAmount = "1"
val gmailText = "Welcome to Gmail"
setupHooks(
additionalAfterSection = {
@ -66,7 +67,7 @@ class FeedbackTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
@ -92,6 +93,9 @@ class FeedbackTest : BaseTestCase() {
step("Click 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert sanding text is displayed") {
onSendConfirmScreen { sendingText.assertIsDisplayed() }
}
step("Click 'Send' button") {
waitForIdle()
onSendConfirmScreen {
@ -100,6 +104,7 @@ class FeedbackTest : BaseTestCase() {
}
}
step("Check 'Failed transaction' dialog") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) {
checkFailedTransactionDialog()
}
@ -107,8 +112,8 @@ class FeedbackTest : BaseTestCase() {
step("Click on 'Support' button") {
onFailedTransactionDialog { supportButton.performClick() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
}
}
}
@ -117,6 +122,8 @@ class FeedbackTest : BaseTestCase() {
@DisplayName("Send feedback: from 'Warning' dialog after card scan")
@Test
fun sendFeedbackFromScanScreenTest() {
val gmailText = "Welcome to Gmail"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
@ -145,8 +152,8 @@ class FeedbackTest : BaseTestCase() {
step("Click on 'Request support' button") {
ScanWarningDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
}
}
}
@ -155,6 +162,8 @@ class FeedbackTest : BaseTestCase() {
@DisplayName("Send feedback: from scan already used wallet alert dialog")
@Test
fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
val gmailText = "Welcome to Gmail"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
@ -175,8 +184,8 @@ class FeedbackTest : BaseTestCase() {
step("Click on 'Request support' button") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
@ -19,13 +18,12 @@ class HideTokenTest : BaseTestCase() {
@Test
fun hideWalletTokenByHideButtonTest() {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }

View file

@ -2,7 +2,6 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
@ -92,13 +91,12 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
@ -179,13 +177,12 @@ class OrganizeTokensTest : BaseTestCase() {
val bitcoinTitle = "Bitcoin"
val polygonTitle = "Polygon"
val polExMaticTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {

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

@ -25,7 +25,6 @@ class SendTest : BaseTestCase() {
val currencyName = "POL (ex-MATIC)"
val feeCurrencyName = "Ethereum"
val feeCurrencySymbol = "ETH"
val balance = "$763.55"
val scenarioName = "eth_network_balance"
val scenarioState = "Empty"
@ -42,7 +41,7 @@ class SendTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)

View file

@ -1,15 +1,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.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
@ -23,7 +21,6 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingBlockTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
@ -41,7 +38,7 @@ class StakingTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
@ -81,7 +78,6 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingMoreScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
val stakingAmount = "1"
@ -100,7 +96,7 @@ class StakingTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
@ -117,116 +113,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()
}
}
}
@ -236,7 +136,6 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Started"
val stakingAmount = "1"
@ -255,7 +154,7 @@ class StakingTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
@ -284,107 +183,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

@ -4,16 +4,15 @@ import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.*
import com.tangem.common.utils.*
import com.tangem.common.utils.resetWireMockScenarios
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -31,7 +30,6 @@ class SwapTokenTest : BaseTestCase() {
fun networkFeeTest() {
val inputAmount = "100"
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
setupHooks().run {
@ -40,7 +38,7 @@ class SwapTokenTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -115,13 +113,12 @@ class SwapTokenTest : BaseTestCase() {
}
).run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -164,13 +161,12 @@ class SwapTokenTest : BaseTestCase() {
val inputAmount = "100"
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }

View file

@ -1,13 +1,16 @@
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.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScanQrScreen
import com.tangem.screens.onWalletConnectScreen
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,8 +24,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() {
val balance = TOTAL_BALANCE
fun openWalletConnectSessionOnMainScreenTest() {
val dAppName = "React App"
val deepLinkUri = getWcUri()
@ -31,25 +33,34 @@ class WalletConnectTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
step("Create WC session buy deeplink") {
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 +68,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,8 +81,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() {
val balance = TOTAL_BALANCE
fun openWalletConnectSessionNotOnMainScreenTest() {
val dAppName = "React App"
val deepLinkUri = getWcUri()
@ -80,43 +90,46 @@ class WalletConnectTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
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,9 @@ class WalletConnectTest : BaseTestCase() {
@DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSession() {
val balance = TOTAL_BALANCE
fun openWalletConnectSessionTest() {
val dAppName = "React App"
val packageName = BuildConfig.APPLICATION_ID
val deepLinkUri = getWcUri()
setupHooks().run {
@ -135,34 +148,30 @@ class WalletConnectTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
synchronizeAddresses()
}
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 +179,71 @@ 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 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()
}
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,394 @@
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.WAIT_UNTIL_TIMEOUT
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 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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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 bitcoinAddress = BITCOIN_ADDRESS
setupHooks(
additionalBeforeSection = {
clearClipboard()
},
additionalAfterSection = {
clearClipboard()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) {
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 url = "sell.moonpay.com"
val useWithoutAccount = "Use without an account"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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") {
ThirdPartyAppPageObject { assertChromeIsOpened() }
}
if (ThirdPartyAppPageObject.isElementWithTextExists(useWithoutAccount)) {
step("Click on '$useWithoutAccount' button on Chrome browser") {
ThirdPartyAppPageObject { clickOnElementWithText(useWithoutAccount) }
}
}
step("Assert url contains: '$url'") {
ThirdPartyAppPageObject { assertUrlContains(url) }
}
}
}
@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"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
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

@ -2,7 +2,6 @@ package com.tangem.tests.balance
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
@ -23,7 +22,7 @@ class TotalBalanceLongTapTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Long tap on total balance block") {
onMainScreen {

View file

@ -51,7 +51,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
synchronizeAddresses(isBalanceAvailable = false)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {
@ -88,7 +88,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
synchronizeAddresses(isBalanceAvailable = false)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {
@ -125,7 +125,7 @@ class TotalBalanceUnavailableTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
synchronizeAddresses(isBalanceAvailable = false)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {

View file

@ -33,7 +33,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
@ -67,7 +67,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
@ -117,7 +117,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
@ -145,7 +145,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
@ -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 {
@ -187,7 +187,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
synchronizeAddresses()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }

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

@ -79,6 +79,16 @@ object MarketsDomainModule {
)
}
@Provides
@Singleton
fun provideGetTokenMarketCryptoCurrency(
marketsTokenRepository: MarketsTokenRepository,
): GetTokenMarketCryptoCurrency {
return GetTokenMarketCryptoCurrency(
marketsTokenRepository = marketsTokenRepository,
)
}
@Provides
@Singleton
fun provideFilterNetworksUseCase(

View file

@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
@ -231,6 +232,18 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideReceiveAddressesFactory(
getEnsNameUseCase: GetEnsNameUseCase,
getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
): ReceiveAddressesFactory {
return ReceiveAddressesFactory(
getEnsNameUseCase = getEnsNameUseCase,
getViewedTokenReceiveWarningUseCase = getViewedTokenReceiveWarningUseCase,
)
}
@Provides
@Singleton
fun provideGetReverseResolvedEnsAddressUseCase(

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

@ -3,8 +3,10 @@ package com.tangem.tap.di.domain
import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
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 +84,68 @@ 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,
)
}
@Provides
@Singleton
fun provideYieldSupplyMinAmountUseCase(
feeRepository: FeeRepository,
quotesRepository: QuotesRepository,
currenciesRepository: CurrenciesRepository,
): YieldSupplyMinAmountUseCase {
return YieldSupplyMinAmountUseCase(
feeRepository = feeRepository,
quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository,
)
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.deserialization.WalletDataDeserializer
import com.tangem.common.extensions.*
import com.tangem.common.map
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
@ -145,11 +144,11 @@ internal class ScanProductTask(
card = cardDto,
session = session,
) { scanResponseResult ->
callback(
scanResponseResult.map { scanResponse ->
scanResponse.copy(visaCardActivationStatus = result.data)
},
)
// callback(
// scanResponseResult.map { scanResponse ->
// scanResponse.copy(visaCardActivationStatus = result.data)
// },
// )
}
}
is CompletionResult.Failure -> {

View file

@ -10,7 +10,7 @@ internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
get() = when (this) {
is UserWallet.Cold -> UserWalletSensitiveInformation(
wallets = scanResponse.card.wallets,
visaCardActivationStatus = scanResponse.visaCardActivationStatus,
// visaCardActivationStatus = scanResponse.visaCardActivationStatus,
mobileWallets = null,
)
is UserWallet.Hot -> UserWalletSensitiveInformation(
@ -30,7 +30,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
// visaCardActivationStatus = null,
),
hasBackupError = hasBackupError,
hotWalletId = null,
@ -80,7 +80,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
card = scanResponse.card.copy(
wallets = requireNotNull(sensitiveInformation.wallets),
),
visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
}
@ -113,7 +113,7 @@ internal fun UserWallet.lock(): UserWallet = when (this) {
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
// visaCardActivationStatus = null,
),
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError

View file

@ -4,7 +4,6 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -20,9 +19,10 @@ object OnboardingHelper {
return when {
response.cardTypesResolver.isVisaWallet() -> {
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
response.visaCardActivationStatus !is VisaCardActivationStatus.Activated
// if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
//
// response.visaCardActivationStatus !is VisaCardActivationStatus.Activated
return true
}
response.cardTypesResolver.isTangemTwins() -> {

View file

@ -110,6 +110,7 @@ internal class WelcomeMiddleware {
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = userWalletsListManager.walletsCount.toString(),
isImported = userWallet.isImported,
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)

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,
@ -593,10 +616,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(
customerWalletAddress = route.customerWalletAddress,
cardNumberEnd = route.cardNumberEnd,
),
params = TangemPayDetailsComponent.Params(config = route.config),
componentFactory = tangemPayDetailsComponentFactory,
)
}

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.feedback.models)
implementation(projects.domain.visa.models)
/* Libs - Other */
api(deps.kotlin.serialization)

View file

@ -18,6 +18,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.pay.TangemPayDetailsConfig
import kotlinx.serialization.Serializable
@SuppressLint("UnsafeOptInUsageError")
@ -51,50 +52,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 +123,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 +175,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 +238,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 +312,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")
@ -442,8 +389,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class TangemPayDetails(
val customerWalletAddress: String,
val cardNumberEnd: String,
val config: TangemPayDetailsConfig,
) : AppRoute(path = "/tangem_pay_details")
@Serializable

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

@ -19,6 +19,7 @@ class AccountPortfolioItemUMConverter(
private val appCurrency: AppCurrency? = null,
private val accountBalance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val isEnabled: Boolean = true,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
) : Converter<Account, UserWalletItemUM> {
@ -29,11 +30,10 @@ class AccountPortfolioItemUMConverter(
name = value.accountName.toUM().value,
information = getInfo(value),
balance = getBalanceInfo(),
isEnabled = true,
isEnabled = isEnabled,
endIcon = endIcon,
onClick = { onClick(value.accountId) },
imageState = getImageState(value),
label = null,
)
}
}

View file

@ -0,0 +1,60 @@
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.platform.testTag
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.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendScreenTestTags
/**
* A composable function that displays an account label (icon + name) with an optional prefix.
*
* Depending on the type of [accountTitleUM], it either shows a prefix text followed by
* an account label (with name and icon) or just a title text.
*
* @param accountTitleUM The data model containing information about the account title.
* @param modifier Optional [Modifier] for styling.
* @param textStyle The [TextStyle] to apply to the text elements. Defaults to subtitle2 style from TangemTheme.
*/
@Composable
fun AccountTitle(
accountTitleUM: AccountTitleUM,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.subtitle2,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
) {
when (accountTitleUM) {
is AccountTitleUM.Account -> {
Text(
text = accountTitleUM.prefixText.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
)
AccountLabel(
name = accountTitleUM.name,
icon = accountTitleUM.icon,
iconSize = AccountIconSize.ExtraSmall,
nameStyle = textStyle,
)
}
is AccountTitleUM.Text -> Text(
text = accountTitleUM.title.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.common.ui.account
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
/**
* A sealed interface representing the title of an account, which can be either a simple text
* or a more complex account representation with a prefix, name, and icon.
*/
@Immutable
sealed interface AccountTitleUM {
/** Represents a simple text title. */
data class Text(
val title: TextReference,
) : AccountTitleUM
/** Represents an account with a prefix, name, and icon. */
data class Account(
val prefixText: TextReference,
val name: TextReference,
val icon: CryptoPortfolioIconUM,
) : AccountTitleUM
}

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

@ -0,0 +1,27 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
import com.tangem.utils.converter.Converter
class AmountAccountConverter(
private val prefixText: TextReference,
private val isAccountsMode: Boolean,
private val walletTitle: TextReference,
) : Converter<Account.CryptoPortfolio?, AccountTitleUM> {
override fun convert(value: Account.CryptoPortfolio?): AccountTitleUM {
return if (value != null && isAccountsMode) {
AccountTitleUM.Account(
name = value.accountName.toUM().value,
icon = value.icon.toUM(),
prefixText = prefixText,
)
} else {
AccountTitleUM.Text(
title = walletTitle,
)
}
}
}

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,114 +1,47 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountTitleUM
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,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val isBalanceHidden: Boolean,
private val accountTitleUM: AccountTitleUM,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverterV2(
AmountFieldConverter(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
@ -118,19 +51,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),
accountTitleUM = accountTitleUM,
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) {
TextReference.EMPTY
@ -145,25 +72,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

@ -1,10 +1,10 @@
package com.tangem.common.ui.amountScreen.models
import androidx.compose.runtime.Stable
import com.tangem.common.ui.account.AccountTitleUM
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 +12,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 accountTitleUM info about current account or wallet
* @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 +27,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 accountTitleUM: AccountTitleUM,
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 +39,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

@ -2,41 +2,33 @@ package com.tangem.common.ui.amountScreen.preview
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
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
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.CryptoPortfolioIcon
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 \$)"),
accountTitleUM = AccountTitleUM.Text(stringReference("Family Wallet")),
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 +57,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 +73,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(
@ -101,6 +84,17 @@ object AmountStatePreviewData {
),
),
)
val amountStateV2Accounts = amountState.copy(
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
prefixText = resourceReference(R.string.common_from),
),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"),
)
val amountErrorState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(
isError = true,

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.account.AccountTitle
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.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,12 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing16),
) {
CurrencyIcon(state = amountState.tokenIconState)
AccountTitle(accountTitleUM = amountState.accountTitleUM)
SpacerH(20.dp)
CurrencyIcon(
state = amountState.tokenIconState,
iconSize = 40.dp,
)
ResizableText(
text = firstAmount,
style = TangemTheme.typography.h2,
@ -68,8 +74,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 +83,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,10 +9,13 @@ 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
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
@ -28,6 +31,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(
@ -63,7 +67,7 @@ fun AmountBlockV2(
val currencyTitle = amount.cryptoAmount.currencySymbol
AmountBlockV2(
title = amountState.title,
accountTitleUM = amountState.accountTitleUM,
balance = amountState.availableBalanceCrypto,
currencyTitle = currencyTitle,
currencyIconState = amountState.tokenIconState,
@ -80,7 +84,7 @@ fun AmountBlockV2(
@Suppress("LongParameterList", "LongMethod")
@Composable
private fun AmountBlockV2(
title: TextReference,
accountTitleUM: AccountTitleUM,
balance: TextReference,
currencyTitle: String,
currencyIconState: CurrencyIconState,
@ -105,11 +109,7 @@ private fun AmountBlockV2(
.padding(TangemTheme.dimens.spacing16),
) {
Row {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
AccountTitle(accountTitleUM)
SpacerWMax()
Text(
text = balance.resolveReference(),
@ -133,6 +133,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 +143,7 @@ private fun AmountBlockV2(
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
)
extraContent()
}
@ -184,6 +186,7 @@ private class AmountBlockV2PreviewProvider : PreviewParameterProvider<AmountStat
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountState,
AmountStatePreviewData.amountStateV2Accounts,
)
}
// endregion

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,16 +16,15 @@ 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.account.AccountTitle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.TextShimmer
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 +32,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,
@ -114,11 +59,7 @@ internal fun LazyListScope.amountFieldV2(
modifier = Modifier.width(60.dp),
)
} else {
Text(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
AccountTitle(amountState.accountTitleUM)
}
AmountFieldV2(
amountUM = amountState,
@ -175,7 +116,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 +125,52 @@ 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 -> {
Text(
text = currentAmount.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 = currentAmount.availableBalanceCrypto.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(
currentAmount.amountTextField.cryptoAmount.currencySymbol.length,
),
modifier = Modifier
.weight(1f, fill = false)
.testTag(SendScreenTestTags.PRIMARY_AMOUNT),
)
EllipsisText(
text = currentAmount.availableBalanceFiat.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(
currentAmount.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

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.Keyboard
@ -17,6 +18,7 @@ import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendConfirmScreenTestTags
/**
* Sending info text with display animation.
@ -52,7 +54,8 @@ fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.testTag(SendConfirmScreenTestTags.SENDING_TEXT),
)
}
}

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
@ -60,40 +57,53 @@ fun UserWalletItem(
onClick = state.onClick,
enabled = state.isEnabled,
) {
Row(
UserWalletItemRow(
state = state,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(state.imageState)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
balance = state.balance,
)
)
}
}
state.label?.let { Label(it) }
@Composable
fun UserWalletItemRow(state: UserWalletItemUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(state.imageState)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
balance = state.balance,
)
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> Unit
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Checkmark -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> Unit
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Checkmark -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Warning -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_circle_24),
tint = TangemTheme.colors.icon.warning,
contentDescription = null,
)
}
}
}
@ -316,10 +326,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

@ -19,12 +19,14 @@ sealed class Basic(
batch: String,
signInType: SignInType,
walletsCount: String,
isImported: Boolean,
hasBackup: Boolean?,
) : Basic(
event = "Signed in",
params = buildMap {
put(AnalyticsParam.CURRENCY, currency.value)
put(AnalyticsParam.BATCH, batch)
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
put("Sign in type", signInType.name)
put("Wallets Count", walletsCount)
if (hasBackup != null) {

View file

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

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.domain.nft.models)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.visa.models)
/** Tangem libraries */
implementation(tangemDeps.blockchain)

View file

@ -16,6 +16,9 @@ enum class ApiEnvironment {
@Json(name = "DEV_2")
DEV_2,
@Json(name = "DEV_3")
DEV_3,
@Json(name = "STAGE")
STAGE,

View file

@ -28,6 +28,7 @@ internal class Express(
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createDev2Environment(),
createDev3Environment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
@ -60,6 +61,12 @@ internal class Express(
headers = createHeaders(isProd = false),
)
private fun createDev3Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV_3,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",

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

@ -73,6 +73,7 @@ internal class TangemTech(
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey

View file

@ -17,6 +17,7 @@ data class CustomerMeResponse(
@Json(name = "product_instance") val productInstance: ProductInstance?,
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "depositAddress") val depositAddress: String?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: Balance?,
)

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,9 +9,8 @@ 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
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -28,6 +27,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")
@ -38,8 +38,8 @@ class MoshiModule {
.add(BigIntegerAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
// .add(VisaActivationRemoteState.jsonAdapter)
// .add(VisaCardActivationStatus.jsonAdapter)
.add(
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
@ -84,8 +84,8 @@ class MoshiModule {
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
return Moshi.Builder().apply {
add(VisaActivationRemoteState.jsonAdapter)
add(VisaCardActivationStatus.jsonAdapter)
// add(VisaActivationRemoteState.jsonAdapter)
// add(VisaCardActivationStatus.jsonAdapter)
adapters.forEach { this.add(it) }
typedAdapters.forEach { add(it.key, it.value) }
addLast(KotlinJsonAdapterFactory())

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

@ -9,7 +9,7 @@ import java.math.BigDecimal
/**
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
*
* @see [com.tangem.domain.tokens.model.NetworkStatus]
* @see [com.tangem.domain.models.network.NetworkStatus]
*/
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface NetworkStatusDM {
@ -41,8 +41,8 @@ sealed interface NetworkStatusDM {
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: Map<String, YieldSupplyStatus?> = emptyMap(),
@Json(name = "amounts") val amounts: List<CurrencyAmount>,
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: List<YieldSupplyStatus>,
) : NetworkStatusDM
/**
@ -107,10 +107,44 @@ sealed interface NetworkStatusDM {
}
}
@JsonClass(generateAdapter = true)
data class CurrencyAmount(
@Json(name = "id") val id: CurrencyId,
@Json(name = "amount") val amount: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class YieldSupplyStatus(
@Json(name = "id") val id: CurrencyId,
@Json(name = "is_active") val isActive: Boolean,
@Json(name = "is_initialized") val isInitialized: Boolean,
@Json(name = "is_allowed_to_spend") val isAllowedToSpend: Boolean,
)
@JsonClass(generateAdapter = true)
data class CurrencyId(
@Json(name = "value") val value: String,
) {
companion object Companion {
const val CONTRACT_ADDRESS_DELIMITER = '\u2693' // ⚓
fun createCoinId(coinId: String): CurrencyId {
return CurrencyId(value = coinId)
}
fun createTokenId(rawTokenId: String?, contractAddress: String): CurrencyId {
return CurrencyId(
value = buildString {
if (rawTokenId != null) {
append(rawTokenId)
}
append(CONTRACT_ADDRESS_DELIMITER)
append(contractAddress)
},
)
}
}
}
}

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

@ -64,9 +64,15 @@ class NetworkStatusDMSerializationTest {
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
yieldSupplyStatuses = mapOf(
"ETH" to NetworkStatusDM.YieldSupplyStatus(
amounts = listOf(
NetworkStatusDM.CurrencyAmount(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
amount = BigDecimal("1.2345"),
),
),
yieldSupplyStatuses = listOf(
NetworkStatusDM.YieldSupplyStatus(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
@ -91,9 +97,15 @@ class NetworkStatusDMSerializationTest {
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
yieldSupplyStatuses = mapOf(
"ETH" to NetworkStatusDM.YieldSupplyStatus(
amounts = listOf(
NetworkStatusDM.CurrencyAmount(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
amount = BigDecimal("1.2345"),
),
),
yieldSupplyStatuses = listOf(
NetworkStatusDM.YieldSupplyStatus(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,

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"))
}
}

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