Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-05 14:01:26 +03:00
commit 9aacdb2ed3
695 changed files with 22022 additions and 5116 deletions

View file

@ -113,12 +113,14 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.models)
implementation(projects.domain.core)
api(projects.domain.common)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.demo.models)
implementation(projects.domain.express)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)

View file

@ -4,8 +4,26 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val ETHEREUM_ADDRESS = "0x9FFd974772bDA94D288240C1B22F367Ce75CcD7f"
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val CARDANO_ADDRESS =
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
const val POLKADOT_RECIPIENT_ADDRESS = "143TfgFYAFfM86LRzt4UcFNU3KosxCndBCVz2U5HCxpLidKZ"
const val XRP_RECIPIENT_ADDRESS = "rNeY28BPda6jp5N5oESZzd2ZN7eMZy8jNf"
const val ENS_NAME = "louded.eth"
const val ENS_ETHEREUM_RECIPIENT_ADDRESS = "0x0211ff638298adcbdc99c177dc2e95ec69948865"
const val ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x0211ff63829..."
const val KUSAMA_RECIPIENT_ADDRESS = "CqNrR92Hh76vW69vDBL5iATrZoYkk9nj67iVSUbb2YHtktn"
const val AZERO_RECIPIENT_ADDRESS = "5EA4p6DZdbt2vLZySML2dG3ZsnNrenEWZHnVCScQh4iq2KZo"
const val TEZOS_RECIPIENT_ADDRESS = "tz1eBdC2JkU2bxgZssweLo6D3wCkWN12ioHW"
const val KASPA_RECIPIENT_ADDRESS = "kaspa:qypc4aywf95rken57kclwdjvycugnacstrh5uzmu0a7pjtxas366ktgj5jzu8t6"
const val XLM_NON_ACTIVATED_RECIPIENT_ADDRESS = "GAGMMENFDWASIHSVO4BPVIT3ZH3YNUIM4EJ6MUDJW46OVPNOJSQIJ22K"
const val XLM_ACTIVATED_RECIPIENT_ADDRESS = "GDKGS3UQFUNQY34P4SIAOKKGEA3NKHH6BSAXK4HIN3BZQQRBTITJLKL6"
const val XRP_NON_ACTIVATED_RECIPIENT_ADDRESS = "rvJfSnN6JzV3rhz1RRKDHnE6MYW28BaZG"
const val XRP_ACTIVATED_RECIPIENT_ADDRESS = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
@ -14,4 +32,7 @@ object TestConstants {
const val ALLURE_LABEL_NAME = "Owner"
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val QUOTES_API_SCENARIO = "quotes_api"
}

View file

@ -20,12 +20,12 @@ fun BaseTestCase.swipeVertical(
)
}
fun BaseTestCase.pullToRefresh() {
fun BaseTestCase.pullToRefresh(steps: Int = 1000) {
swipeVertical(
direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f,
endHeightRatio = 0.8f,
steps = 1000
steps = steps
)
}

View file

@ -115,4 +115,28 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
}
fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) {
if (isEnabled) {
step("Assert 'Buy' button is enabled") {
onMainScreen { buyButton.assertIsEnabled() }
}
step("Assert 'Swap' button is enabled") {
onMainScreen { swapButton.assertIsEnabled() }
}
step("Assert 'Sell' button is enabled") {
onMainScreen { sellButton.assertIsEnabled() }
}
} else {
step("Assert 'Buy' button is not enabled") {
onMainScreen { buyButton.assertIsNotEnabled() }
}
step("Assert 'Swap' button is not enabled") {
onMainScreen { swapButton.assertIsNotEnabled() }
}
step("Assert 'Sell' button is not enabled") {
onMainScreen { sellButton.assertIsNotEnabled() }
}
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onActionIsUnavailableDialog
import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step
@ -63,4 +64,16 @@ fun checkAlreadyUsedWalletDialog() {
step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() }
}
}
fun BaseTestCase.checkActionIsUnavailableDialog() {
step("Assert 'Action is unavailable' dialog title is displayed") {
onActionIsUnavailableDialog { title.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog text is displayed") {
onActionIsUnavailableDialog { text.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog 'Ok' button is displayed") {
onActionIsUnavailableDialog { okButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onReceiveAssetsBottomSheet
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
import com.tangem.screens.onTokenReceiveWarningBottomSheet
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.goToQrCodeBottomSheet() {
step("Assert 'Token receive warning' bottom sheet is displayed") {
onTokenReceiveWarningBottomSheet { bottomSheet.assertIsDisplayed() }
}
step("Click on 'Got it' button") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
}
fun BaseTestCase.checkQrCodeBottomSheetScenario() {
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() }
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkNetworkFeeBlock(currentFeeAmount: String, withFeeSelector: Boolean) {
step("Assert fee selector block is displayed") {
onSendConfirmScreen { feeSelectorBlock.assertIsDisplayed() }
}
step("Assert fee selector icon is displayed") {
onSendConfirmScreen { feeSelectorIcon.assertIsDisplayed() }
}
step("Assert fee selector title is displayed") {
onSendConfirmScreen { feeSelectorTitle.assertIsDisplayed() }
}
step("Assert fee selector tooltip icon is displayed") {
onSendConfirmScreen { feeSelectorTooltipIcon.assertIsDisplayed() }
}
step("Assert fee amount = '$currentFeeAmount'") {
onSendConfirmScreen { feeAmount.assertTextContains(currentFeeAmount) }
}
if (withFeeSelector) {
step("Assert select fee icon is displayed") {
onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() }
}
}
}
fun BaseTestCase.openSendConfirmScreen(
tokenName: String,
inputAmount: String,
recipientAddress: String
) {
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
}

View file

@ -0,0 +1,84 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onTokenDetailsScreen
import io.github.kakaocup.compose.node.element.KNode
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openSendScreen(tokenName: String, mockState: String = "") {
val scenarioState = mockState.ifEmpty { tokenName }
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
}
fun BaseTestCase.checkSendWarning(
title: String,
message: String,
isDisplayed: Boolean = true,
sendButtonIsDisabled: Boolean = isDisplayed,
) {
val assertDisplay = if (isDisplayed) "displayed" else "not displayed"
step("Assert 'Send confirm screen' is displayed") {
onSendConfirmScreen {
appBarTitle.assertIsDisplayed()
}
}
step("Assert warning title is $assertDisplay") {
onSendConfirmScreen {
warningTitle(title).assertVisibility(isDisplayed)
}
}
step("Assert warning icon is $assertDisplay") {
onSendConfirmScreen {
sendWarningIcon(message).assertVisibility(isDisplayed)
}
}
step("Assert warning message is $assertDisplay") {
onSendConfirmScreen {
sendWarningMessage(message).assertVisibility(isDisplayed)
}
}
if (sendButtonIsDisabled)
step("Assert 'Send' button is disabled") {
onSendConfirmScreen {
sendButton.assertIsNotEnabled()
}
}
else
step("Assert 'Send' button is enabled") {
onSendConfirmScreen {
sendButton.assertIsEnabled()
}
}
}
private fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
if (shouldBeDisplayed) {
assertIsDisplayed()
} else {
assertIsNotDisplayed()
}
}

View file

@ -0,0 +1,40 @@
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.BaseDialogTestTags
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.common.ui.R as CommonUIR
class ActionIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ActionIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_message))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onActionIsUnavailableDialog(function: ActionIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,14 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))

View file

@ -0,0 +1,30 @@
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.BaseDialogTestTags
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.common.ui.R as CommonUIR
class OperationIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<OperationIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.token_button_unavailability_generic_description))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onOperationIsUnavailableDialog(function: OperationIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,24 @@
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.*
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 SellPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SellPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_sell))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSellScreen(function: SellPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -9,18 +9,94 @@ 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
import com.tangem.core.ui.R as CoreUiR
class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val addressTextFieldTitle: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE)
useUnmergedTree = true
}
val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true
}
val addressPasteButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON)
useUnmergedTree = true
}
val clearTextFieldButton: KNode = child {
hasContentDescription(getResourceString(CoreUiR.string.common_close))
useUnmergedTree = true
}
val resolvedAddress: KNode = child {
hasTestTag(SendAddressScreenTestTags.RESOLVED_ADDRESS)
}
val recentAddressesTitle: KNode = child {
hasText(getResourceString(CoreUiR.string.send_recent_transactions))
useUnmergedTree = true
}
fun destinationTagBlockTitle(isMemoCorrectOrEmpty: Boolean = true): KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD_TITLE)
useUnmergedTree = true
if(isMemoCorrectOrEmpty) {
hasText(getResourceString(CoreUiR.string.send_destination_tag_field))
} else {
hasText(getResourceString(CoreUiR.string.send_memo_destination_tag_error))
}
}
val destinationTagTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)
useUnmergedTree = true
}
val destinationTagBlockText: KNode = child {
hasText(
getResourceString(CoreUiR.string.send_recipient_memo_footer_v2) + "\n" +
getResourceString(CoreUiR.string.send_recipient_memo_footer_v2_highlighted)
)
useUnmergedTree = true
}
val destinationTagBlockCaution: KNode = child {
hasText(getResourceString(CoreUiR.string.send_recipient_memo_footer_v2_highlighted))
}
val destinationTagPasteButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_PASTE_BUTTON)
useUnmergedTree = true
}
val clearDestinationTagTextFieldButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_next)))
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_next))
hasText(getResourceString(R.string.common_continue))
useUnmergedTree = true
}
fun recentAddressWithText(recipientAddress: String): KNode = child {
hasParent(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TITLE))
hasText(recipientAddress)
useUnmergedTree = true
}

View file

@ -2,36 +2,55 @@ 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.NotificationTestTags
import com.tangem.core.ui.test.SendConfirmScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.*
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
import androidx.compose.ui.test.hasText as withText
import com.tangem.common.ui.R as CommonUiR
import androidx.compose.ui.test.hasTestTag as withTestTag
import com.tangem.core.ui.R as CoreUiR
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
val appBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_send))
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val minimumSendAmountErrorTitle: KNode = child {
val secondaryAmount: KNode = child {
hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
fun leaveDepositButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_leave_button, amount)))
useUnmergedTree = true
}
fun reduceAmountButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_reduce_by, amount)))
useUnmergedTree = true
}
fun warningTitle(title: String): KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(CommonUiR.string.send_notification_invalid_amount_title))
hasText(title)
useUnmergedTree = true
}
@ -40,34 +59,76 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true
}
fun minimumSendAmountErrorIcon(amount: String): KNode = child {
hasAnySibling(
withText(
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
amount,
amount,
)
)
)
fun sendWarningIcon(message: String): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling(withText(message))
useUnmergedTree = true
}
fun minimumSendAmountErrorMessage(
amount: String,
fun sendWarningMessage(
message: String,
): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
amount,
amount,
)
)
hasText(message)
useUnmergedTree = true
}
fun warningMessage(messageResId: Int): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(messageResId))
useUnmergedTree = true
}
fun warningIcon(message: String): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling(withText(message))
useUnmergedTree = true
}
fun recipientAddress(recipientAddress: String): KNode = child {
hasTestTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS)
hasText(recipientAddress)
useUnmergedTree = true
}
val blockchainAddress: KNode = child {
hasTestTag(SendConfirmScreenTestTags.BLOCKCHAIN_ADDRESS)
useUnmergedTree = true
}
val feeSelectorBlock: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
}
val feeSelectorIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.ICON)
useUnmergedTree = true
}
val feeSelectorTitle: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.TITLE)
useUnmergedTree = true
}
val feeSelectorTooltipIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.TOOLTIP_ICON)
useUnmergedTree = true
}
val selectFeeIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON)
useUnmergedTree = true
}
val feeAmount: KNode = child {
hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT))
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreUiR.string.warning_button_refresh))
}
}
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =

View file

@ -38,6 +38,16 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val equivalentInputAmount: KNode = child {
hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT)
useUnmergedTree = true
}
val exchangeIcon: KNode = child {
hasTestTag(SendScreenTestTags.EXCHANGE_ICON)
useUnmergedTree = true
}
val tokenName: KNode = child {
hasTestTag(SendScreenTestTags.TOKEN_NAME)
useUnmergedTree = true
@ -64,6 +74,12 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_continue))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) =

View file

@ -0,0 +1,35 @@
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.BaseDialogTestTags
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.common.ui.R as CommonUIR
class SwapIsNotSupportedDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapIsNotSupportedDialogPageObject>(semanticsProvider = semanticsProvider) {
fun text(currencyName: String): KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(
getResourceString(
CommonUIR.string.token_button_unavailability_reason_not_exchangeable,
currencyName
)
)
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onSwapIsNotSupportedDialog(function: SwapIsNotSupportedDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -97,25 +97,31 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
)
@OptIn(ExperimentalTestApi::class)
val swapButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
fun receiveButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_receive))
}
@OptIn(ExperimentalTestApi::class)
fun swapButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_swap))
}
@OptIn(ExperimentalTestApi::class)
val sellButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
fun sellButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_sell))
}
@OptIn(ExperimentalTestApi::class)
val buyButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
fun buyButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy))
}
@OptIn(ExperimentalTestApi::class)
val sendButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
fun sendButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_send))
}

View file

@ -2,14 +2,23 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.pullToRefresh
import com.tangem.common.ui.R
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSendScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@ -26,28 +35,18 @@ class BlockchainTest : BaseTestCase() {
val validSendAmount = "10"
val minAmount = "ADA 1.00"
val address = CARDANO_ADDRESS
val scenarioName = "user_tokens_api"
val scenarioState = "Cardano"
val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
val invalidAmountMessage =
getResourceString(R.string.send_notification_invalid_minimum_amount_text, minAmount, minAmount)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$errorSendAmount' in input text field") {
onSendScreen {
@ -64,47 +63,30 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' error title is displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsDisplayed() }
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
)
}
step("Assert 'Invalid amount' error icon is displayed") {
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsDisplayed() }
}
step("Assert 'Invalid amount' error message is displayed") {
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsDisplayed() }
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert address text field is displayed") {
onSendAddressScreen { addressTextField.assertIsDisplayed() }
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$validSendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(errorSendAmount)
amountInputTextField.performTextReplacement(validSendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert address text field is displayed") {
onSendAddressScreen { addressTextField.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' error title is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsNotDisplayed() }
}
step("Assert 'Invalid amount' error icon is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsNotDisplayed() }
}
step("Assert 'Invalid amount' error message is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsNotDisplayed() }
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
@ -136,10 +118,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoErrorState)
setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoErrorState
)
}
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesErrorState)
setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesErrorState
)
}
step("Open 'Main Screen'") {
openMainScreen()
@ -169,10 +157,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoStartedState)
setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoStartedState
)
}
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesStartedState)
setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesStartedState
)
}
step("Pull to refresh") {
pullToRefresh()

View file

@ -15,7 +15,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@ -73,7 +72,7 @@ class FeedbackTest : BaseTestCase() {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
@ -130,7 +129,7 @@ class FeedbackTest : BaseTestCase() {
MockProvider.resetEmulateError()
}
).run {
Allure.step("Click on 'Accept' button") {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Set scanning error") {

View file

@ -47,7 +47,7 @@ class SwapTokenTest : BaseTestCase() {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
onTokenDetailsScreen { swapButton().performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
@ -131,7 +131,7 @@ class SwapTokenTest : BaseTestCase() {
disableMobileData()
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
onTokenDetailsScreen { swapButton().performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
@ -175,7 +175,7 @@ class SwapTokenTest : BaseTestCase() {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
onTokenDetailsScreen { swapButton().performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }

View file

@ -2,14 +2,19 @@ package com.tangem.tests.actionButtons
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
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.common.extensions.*
import com.tangem.common.utils.*
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -281,41 +286,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Click on 'Receive' button") {
onTokenActionsBottomSheet { receiveButton.performClick() }
}
step("Assert 'Token receive warning' bottom sheet is displayed") {
waitForIdle()
step("Go to QR code bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onTokenReceiveWarningBottomSheet {
bottomSheet.assertIsDisplayed()
}
goToQrCodeBottomSheet()
}
}
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() }
step("Check QR code bottom sheet") {
checkQrCodeBottomSheetScenario()
}
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("85")
@DisplayName("Action buttons (long tap): check 'Sell' button")
@Test
@ -342,10 +324,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
}
}
}
step("Assert 'Receive' button is displayed") {
step("Assert 'Sell' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
}
step("Click on 'Receive' button") {
step("Click on 'Sell' button") {
onTokenActionsBottomSheet { sellButton.performClick() }
}
step("Assert Chrome Browser is opened") {
@ -391,4 +373,258 @@ class MainScreenActionButtonsTest : BaseTestCase() {
}
}
}
@AllureId("895")
@DisplayName("Action buttons: check blockchain information by click on 'Buy' button")
@Test
fun checkClickOnBuyButtonOnMainTest() {
val cardType: MockContent = TwinsMockContent
val cardName = "Twin"
val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
setupHooks().run {
step("Open 'Main Screen' on '$cardName' card") {
openMainScreen(mockContent = cardType, isTwinsCard = true)
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { 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() }
}
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("4395")
@DisplayName("Action buttons (main screen): click on buttons with success response")
@Test
fun clickOnActionButtonsWithSuccessResponseTest() {
val tokenTitle = "Ethereum"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Assert 'Buy' screen title is displayed") {
onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() }
}
step("Assert token with title: '$tokenTitle' is displayed") {
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Click on close button on stories screen") {
onSwapStoriesScreen { closeButton.performClick() }
}
step("Assert 'Swap' token screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Assert 'Sell' token screen title is displayed") {
onSellScreen { title.assertIsDisplayed() }
}
}
}
@AllureId("4396")
@DisplayName("Action buttons (main screen): click on buttons without data")
@Test
fun clickOnActionButtonsWithoutDataTest() {
setupHooks(
additionalAfterSection = {
enableWiFi()
enableMobileData()
}
).run {
step("Turn off internet") {
disableWiFi()
disableMobileData()
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
}
}
@AllureId("4398")
@DisplayName("Action buttons (main screen): click on buttons with error response")
@Test
fun clickOnActionButtonsWithErrorResponseTest() {
val scenarioName = "express_api_assets"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
}
}
@AllureId("3642")
@DisplayName("Action buttons (main screen): check buttons state")
@Test
fun checkButtonsStateTest() {
val scenarioName = "user_tokens_api"
val scenarioState = "EmptyTokensList"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
step("Reset Wiremock scenario: '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
}
}
}

View file

@ -0,0 +1,255 @@
package com.tangem.tests.actionButtons
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
import com.tangem.scenarios.goToQrCodeBottomSheet
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSwapStoriesScreen
import com.tangem.screens.onSwapTokenScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
@AllureId("594")
@DisplayName("Action buttons (token details screen): validate UI")
@Test
fun actionButtonsValidateUiTest() {
val tokenTitle = "Bitcoin"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is displayed") {
onTokenDetailsScreen { receiveButton().assertIsDisplayed() }
}
step("Assert 'Buy' button is displayed") {
onTokenDetailsScreen { buyButton().assertIsDisplayed() }
}
step("Assert 'Send' button is displayed") {
onTokenDetailsScreen { sendButton().assertIsDisplayed() }
}
step("Assert 'Swap' button is displayed") {
onTokenDetailsScreen { swapButton().assertIsDisplayed() }
}
step("Assert 'Sell' button is displayed") {
onTokenDetailsScreen { sellButton().assertIsDisplayed() }
}
}
}
@AllureId("593")
@DisplayName("Action buttons (token details screen): check buttons state")
@Test
fun checkActionButtonsStateTest() {
val tokenTitle = "Bitcoin"
val actionButtonIsNotDimmed = "Action button is not dimmed"
val actionButtonIsDimmed = "Action button is dimmed"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is not dimmed") {
onTokenDetailsScreen { receiveButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Buy' button is not dimmed") {
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Send' button is dimmed") {
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Assert 'Sell' button is dimmed") {
onTokenDetailsScreen { sellButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("4459")
@DisplayName("Action buttons (token details screen): check 'Swap' button (success)")
@Test
fun checkSwapButtonSuccessTest() {
val tokenTitle = "Ethereum"
val tokenSymbol = "ETH"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { 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("4460")
@DisplayName("Action buttons (token details screen): check 'Swap' button (provider error)")
@Test
fun checkSwapButtonProviderErrorTest() {
val tokenTitle = "POL (ex-MATIC)"
val actionButtonIsDimmed = "Action button is dimmed"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton().performClick() }
}
step("Assert swapping $tokenTitle is not supported dialog text is displayed") {
onSwapIsNotSupportedDialog { text(tokenTitle).assertIsDisplayed() }
}
step("Assert 'Ok' button is displayed") {
onSwapIsNotSupportedDialog { okButton.assertIsDisplayed() }
}
step("Click on 'Ok' button") {
onSwapIsNotSupportedDialog { okButton.performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("4461")
@DisplayName("Action buttons (token details screen): check 'Swap' button (Express error)")
@Test
fun checkSwapButtonExpressErrorTest() {
val tokenTitle = "Polygon"
val actionButtonIsDimmed = "Action button is dimmed"
val scenarioName = "express_api_assets"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton().performClick() }
}
step("Assert operation is unavailable dialog text is displayed") {
onOperationIsUnavailableDialog { text.assertIsDisplayed() }
}
step("Assert 'Ok' button is displayed") {
onOperationIsUnavailableDialog { okButton.assertIsDisplayed() }
}
step("Click on 'Ok' button") {
onOperationIsUnavailableDialog { okButton.performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("3590")
@DisplayName("Action buttons (token details screen): validate UI")
@Test
fun checkReceiveButtonTest() {
val tokenTitle = "Bitcoin"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is displayed") {
onTokenDetailsScreen { receiveButton().performClick() }
}
step("Go to QR code bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
goToQrCodeBottomSheet()
}
}
step("Check QR code bottom sheet") {
checkQrCodeBottomSheetScenario()
}
}
}
}

View file

@ -0,0 +1,338 @@
package com.tangem.tests.send.addressScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
import com.tangem.common.constants.TestConstants.ENS_NAME
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.*
import com.tangem.core.ui.R
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.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SendAddressScreenTest : BaseTestCase() {
@AllureId("4006")
@DisplayName("Send (address screen): check address history")
@Test
fun sendAddressHistoryTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val recipientShortenedAddress = ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Click on previous address") {
onSendAddressScreen { recentAddressWithText(recipientShortenedAddress).clickWithAssertion() }
}
step("Assert recipient address is displayed") {
onSendConfirmScreen { recipientAddress(recipientAddress).assertIsDisplayed() }
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert address text field contains correct recipient address") {
onSendAddressScreen { addressTextField.assertTextContains(recipientAddress) }
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Assert recipient text field is empty") {
onSendAddressScreen { addressTextField.assertTextEquals("") }
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4004")
@DisplayName("Send (address screen): check destination tag")
@Test
fun sendDestinationTagTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val correctMemo = "123"
val invalidMemo = "hz"
val xrpRecipientAddress = XRP_RECIPIENT_ADDRESS
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val context = device.context
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Set clipboard text") {
setClipboardText(context, correctMemo)
}
step("Assert 'Destination Tag' title is displayed") {
onSendAddressScreen { destinationTagBlockTitle().assertIsDisplayed() }
}
step("Assert 'Destination Tag' text is displayed") {
onSendAddressScreen { destinationTagBlockText.assertIsDisplayed() }
}
step("CLick on 'Paste' button") {
onSendAddressScreen { destinationTagPasteButton.clickWithAssertion() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Click on 'Clear text field button'") {
onSendAddressScreen { clearDestinationTagTextFieldButton.clickWithAssertion() }
}
step("Type invalid memo in input text field") {
onSendAddressScreen { destinationTagTextField.performTextReplacement(invalidMemo) }
}
step("Assert 'Invalid memo' title is displayed") {
onSendAddressScreen { destinationTagBlockTitle(isMemoCorrectOrEmpty = false).assertIsDisplayed() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4543")
@DisplayName("Send (address screen): check address field")
@Test
fun sendAddressFieldTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val invalidAddress = "s"
val walletAddress = ETHEREUM_ADDRESS
val context = device.context
val recipient = getResourceString(R.string.send_recipient)
val notAValidAddress = getResourceString(R.string.send_recipient_address_error)
val sameAsWalletAddress = getResourceString(R.string.send_error_address_same_as_wallet)
setupHooks(
additionalAfterSection = {
clearClipboard()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Set clipboard text") {
setClipboardText(context, recipientAddress)
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains correct recipient address") {
onSendAddressScreen { addressTextField.assertTextContains(recipientAddress) }
}
step("Assert address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(recipient) }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Set clipboard text") {
setClipboardText(context, invalidAddress)
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains invalid address") {
onSendAddressScreen { addressTextField.assertTextContains(invalidAddress) }
}
step("Assert invalid address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
step("Set clipboard text") {
setClipboardText(context, walletAddress)
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains invalid address") {
onSendAddressScreen { addressTextField.assertTextContains(walletAddress) }
}
step("Assert 'Address is the same as wallet address' error title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4004")
@DisplayName("Send (address screen): check ENS name")
@Test
fun sendEnsNameTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val ensName = ENS_NAME
val invalidEnsName = "l"
val ensAddress = ENS_ETHEREUM_RECIPIENT_ADDRESS
val ensShortenedAddress = ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
val scenarioName = "eth_call_api"
val scenarioState = "EnsName"
val notAValidAddress = getResourceString(R.string.send_recipient_address_error)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type ENS name: '$ensName' in text field") {
onSendAddressScreen { addressTextField.performTextReplacement(ensName) }
}
step("Assert ENS address displayed") {
onSendAddressScreen { resolvedAddress.assertTextContains(ensAddress, substring = true) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert recipient address is displayed") {
onSendConfirmScreen { recipientAddress(ensName).assertIsDisplayed() }
}
step("Assert blockchain address is displayed") {
onSendConfirmScreen { blockchainAddress.assertTextContains(ensShortenedAddress, substring = true) }
}
step("Click on recipient address") {
onSendConfirmScreen { recipientAddress(ensName).performClick() }
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Type invalid ENS name: '$invalidEnsName' in text field") {
onSendAddressScreen { addressTextField.performTextReplacement(invalidEnsName) }
}
step("Assert invalid address error title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
}
}
}
}

View file

@ -0,0 +1,300 @@
package com.tangem.tests.send.confirmScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.*
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SendConfirmScreenTest : BaseTestCase() {
@AllureId("4003")
@DisplayName("Send (Confirm screen): change sending amount")
@Test
fun changeSendingAmountTest() {
val tokenName = "Ethereum"
val inputAmount = "1"
val newInputAmount = "0.9"
val ethereumAmount = "1.00"
val fiatAmount = "$2,535.63"
val newFiatAmount = "$2,282.07"
val newEthereumAmount = "0.90"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert token amount = '$ethereumAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(ethereumAmount) }
}
step("Assert fiat amount = '$fiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) }
}
step("Click on token amount") {
onSendConfirmScreen { primaryAmount.performClick() }
}
step("Clear input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextClearance()
}
}
step("Type '$newInputAmount' in input text field") {
onSendScreen {
amountInputTextField.performTextReplacement(newInputAmount)
}
}
step("Assert fiat amount = '$newFiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(newFiatAmount) }
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert token amount = '$newEthereumAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(newEthereumAmount) }
}
step("Assert fiat amount = '$newFiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(newFiatAmount) }
}
}
}
@AllureId("552")
@DisplayName("Send (Confirm screen): switch to equivalent")
@Test
fun switchToEquivalentTest() {
val tokenName = "Ethereum"
val inputAmount = "1"
val tokenAmount = "1.00"
val ethereumAmount = "ETH 1.00"
val fiatAmount = "$2,535.63"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on exchange icon") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert token amount = '$ethereumAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(ethereumAmount) }
}
step("Click on exchange icon") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert primary amount = '$tokenAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(tokenAmount) }
}
step("Assert secondary amount = '$fiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) }
}
}
}
@AllureId("553")
@DisplayName("Send (Confirm screen): check fee for blockchain with unknown fee")
@Test
fun checkFeeForBlockchainWithUnknownFeeTest() {
val tokenName = "Polygon"
val inputAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val currentFeeAmount = "<$0.01"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up to token") {
swipeVertical(SwipeDirection.UP)
}
step("Open 'Send Confirm' screen for token '$tokenName'") {
openSendConfirmScreen(
tokenName = tokenName,
inputAmount = inputAmount,
recipientAddress = recipientAddress
)
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("4565")
@DisplayName("Send (Confirm screen): check fee for token with unknown fee")
@Test
fun checkFeeForTokenWithUnknownFeeTest() {
val tokenName = "POL (ex-MATIC)"
val inputAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val currentFeeAmount = "<$0.01"
val scenarioName = "eth_estimate_gas"
val scenarioState = "UnknownFee"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up to token") {
swipeVertical(SwipeDirection.UP)
}
step("Open 'Send Confirm' screen for token '$tokenName'") {
openSendConfirmScreen(
tokenName = tokenName,
inputAmount = inputAmount,
recipientAddress = recipientAddress
)
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("554")
@DisplayName("Send (Confirm screen): check fee warning")
@Test
fun checkFeeWarningTest() {
val tokenName = "Polkadot"
val tokenAmount = "0.1"
val warningTitle = getResourceString(R.string.send_fee_unreachable_error_title)
val warningMessageResId = R.string.send_fee_unreachable_error_text
val currentFeeAmount = "$0.05"
setupHooks(
additionalAfterSection = {
enableWiFi()
enableMobileData()
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Turn off internet") {
disableWiFi()
disableMobileData()
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Turn on internet") {
enableWiFi()
enableMobileData()
}
step("Assert 'Network fee info unreachable' warning title is displayed") {
onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() }
}
step("Assert 'Check your internet connection' warning message is displayed") {
onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() }
}
step("Assert warning icon is displayed") {
onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() }
}
step("Click on 'Refresh' button") {
waitForIdle()
onSendConfirmScreen { refreshButton.clickWithAssertion() }
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = false)
}
}
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.AZERO_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class AzeroWarningsTest : BaseTestCase() {
private val tokenName = "Aleph Zero"
private val amountToLeaveLessThanDeposit = "0.099876587544"
private val amountToLeaveGreaterThanDeposit = "0.09"
private val depositAmount = "AZERO 0.0000000005"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(
R.string.send_notification_existential_deposit_text, depositAmount
)
@AllureId("4290")
@DisplayName("Warnings: check deposit warnings in Azero")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(AZERO_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.KUSAMA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class KusamaWarningsTest : BaseTestCase() {
private val tokenName = "Kusama"
private val amountToLeaveLessThanDeposit = "0.300333"
private val amountToLeaveGreaterThanDeposit = "0.1"
private val depositAmount = "KSM 0.000333333333"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(
R.string.send_notification_existential_deposit_text, depositAmount
)
@AllureId("4291")
@DisplayName("Warnings: check deposit warnings in Kusama")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(KUSAMA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class PolkadotWarningsTest : BaseTestCase() {
private val tokenName = "Polkadot"
private val amountToLeaveLessThanDeposit = "1.299"
private val amountToLeaveGreaterThanDeposit = "0.2"
private val depositAmount = "DOT 1.00"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(
R.string.send_notification_existential_deposit_text, depositAmount
)
@AllureId("4289")
@DisplayName("Warnings: check deposit warnings in Polkadot")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,180 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SolanaWarningsTest : BaseTestCase() {
private val tokenName = "Solana"
private val amountToLeaveLessThanRent = "0.0016941"
private val amountToLeaveGreaterThanRent = "0.0000941"
private val amountToLeaveRentOnly = "0.00168934"
private val rentAmount = "SOL 0.00089088"
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
private val invalidAmountMessage = getResourceString(R.string.send_notification_invalid_amount_rent_fee, rentAmount)
@AllureId("564")
@DisplayName("Warnings: warning is displayed, if after send balance is less than rent amount (SOLANA)")
@Test
fun warningIsDisplayedWhenLeaveLessThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage
)
}
}
}
@AllureId("567")
@DisplayName("Warnings: warning is not displayed, if after send balance is greater than rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveGreaterThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveGreaterThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
@AllureId("566")
@DisplayName("Warnings: warning is not displayed, if after send balance is equal to rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveOnlyRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveRentOnly' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveRentOnly)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
@AllureId("565")
@DisplayName("Warnings: warning is not displayed, if after send balance is zero (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveZeroSol() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type max amount in input text field") {
onSendScreen {
maxButton.performClick()
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,195 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.XLM_ACTIVATED_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.XLM_NON_ACTIVATED_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class StellarWarningsTest : BaseTestCase() {
private val tokenName = "Stellar"
private val mockStateName = "XLM"
private val lessThanReserveAmount = "0.5"
private val equalToReserveAmount = "1"
private val reserveAmount = "XLM 1.00"
private val greaterThanReserveAmount = "2"
private val warningTitle =
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
@AllureId("4287")
@DisplayName("Warnings: check warning, when sending less than reserve")
@Test
fun checkWarningWhenSendingLessThanReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$lessThanReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(lessThanReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4286")
@DisplayName("Warnings: check warning when sending amount equal to reserve")
@Test
fun checkWarningWhenSendingAmountEqualToReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$equalToReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(equalToReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4288")
@DisplayName("Warnings: check warning when sending greater than reserve")
@Test
fun checkWarningWhenSendingGreaterThanReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$greaterThanReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(greaterThanReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XLM_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,120 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.TEZOS_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
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.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class TezosWarningsTest : BaseTestCase() {
private val tokenName = "Tezos"
private val reduceAmount = "0.000001"
private val sendAmount = "0.01"
private val feeIsHighTitle = getResourceString(R.string.send_notification_high_fee_title)
private val feeIsHighMessage =
getResourceString(R.string.send_notification_high_fee_text, tokenName, reduceAmount)
@AllureId("4229")
@DisplayName("Warnings: warning is displayed when sending max amount")
@Test
fun warningIsDisplayedWhenSendMaxAmount() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Click 'Max amount' button") {
onSendScreen {
maxButton.clickWithAssertion()
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(TEZOS_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Swipe up to see the warning") {
swipeVertical(SwipeDirection.UP)
}
step("Assert 'Fee is high warning' is displayed") {
checkSendWarning(
title = feeIsHighTitle,
message = feeIsHighMessage,
sendButtonIsDisabled = false
)
}
step("Click on 'Reduce' button") {
onSendConfirmScreen { reduceAmountButton(reduceAmount).clickWithAssertion() }
}
step("Assert 'Fee is high warning' is not displayed") {
checkSendWarning(
title = feeIsHighTitle,
message = feeIsHighMessage,
isDisplayed = false
)
}
}
}
@AllureId("4230")
@DisplayName("Warnings: warning is not displayed when sending not max amount")
@Test
fun warningIsNotDisplayedWhenSendNotMaxAmount() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(TEZOS_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Fee is high warning' is not displayed") {
checkSendWarning(
title = feeIsHighTitle,
message = feeIsHighMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,195 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.XRP_ACTIVATED_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_NON_ACTIVATED_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class XRPWarningsTest : BaseTestCase() {
private val tokenName = "XRP Ledger"
private val mockStateName = "XRP"
private val lessThanReserveAmount = "0.5"
private val equalToReserveAmount = "1"
private val reserveAmount = "XRP 1.00"
private val greaterThanReserveAmount = "2"
private val warningTitle =
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
@AllureId("4255")
@DisplayName("Warnings: check warning, when sending less than reserve")
@Test
fun checkWarningWhenSendingLessThanReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$lessThanReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(lessThanReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4285")
@DisplayName("Warnings: check warning when sending amount equal to reserve")
@Test
fun checkWarningWhenSendingAmountEqualToReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$equalToReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(equalToReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4284")
@DisplayName("Warnings: check warning when sending greater than reserve")
@Test
fun checkWarningWhenSendingGreaterThanReserve() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$greaterThanReserveAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(greaterThanReserveAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type non activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
step("Click on 'Address' field") {
onSendConfirmScreen { recipientAddress(XRP_NON_ACTIVATED_RECIPIENT_ADDRESS).clickWithAssertion() }
}
step("Type an activated address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(XRP_ACTIVATED_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Invalid reserve amount warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
}

View file

@ -310,4 +310,8 @@
</intent-filter>
</service>
</application>
<queries>
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
<package android:name="com.google.android.apps.walletnfcrel" />
</queries>
</manifest>

View file

@ -3,10 +3,13 @@ package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.addHotWalletContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.setHotWalletContext
/**
[REDACTED_AUTHOR]
@ -17,6 +20,14 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.setContext(scanResponse)
}
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() {
Analytics.eraseContext()
}
@ -25,6 +36,10 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.addContext(scanResponse)
}
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() {
Analytics.removeContext()
}

View file

@ -6,6 +6,7 @@ class BlockchainApiExceptionEvent(
selectedHost: String,
exceptionHost: String,
error: String,
blockchain: String,
) : AnalyticsEvent(
category = "BlockchainSdk",
event = "Exception",
@ -13,5 +14,6 @@ class BlockchainApiExceptionEvent(
AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost,
AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost,
AnalyticsParam.ERROR_DESCRIPTION to error,
AnalyticsParam.BLOCKCHAIN to blockchain,
),
)

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent
import javax.inject.Inject
@ -8,12 +10,13 @@ import javax.inject.Inject
class BlockchainExceptionHandler @Inject constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : ExceptionHandlerOutput {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String, blockchain: Blockchain) {
analyticsErrorHandler.sendErrorEvent(
BlockchainApiExceptionEvent(
selectedHost = nextHost,
exceptionHost = currentHost,
error = message,
blockchain = blockchain.toNetworkId(),
),
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
) : ParamsInterceptor {
override fun id(): String = HotWalletContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.PRODUCT_TYPE] = "Mobile Wallet"
}
companion object {
fun id(): String = HotWalletContextInterceptor::class.java.simpleName
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.domain.models.scan.ScanResponse
*/
class LinkedCardContextInterceptor(
scanResponse: ScanResponse,
val parent: LinkedCardContextInterceptor? = null,
val parent: ParamsInterceptor? = null,
) : ParamsInterceptor {
private val contextInterceptor = CardContextInterceptor(scanResponse)

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/**
@ -24,19 +25,46 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
when (userWallet) {
is UserWallet.Cold -> {
removeParamsInterceptor(HotWalletContextInterceptor.id())
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
}
is UserWallet.Hot -> {
removeParamsInterceptor(LinkedCardContextInterceptor.id())
addParamsInterceptor(HotWalletContextInterceptor())
}
}
}
fun Analytics.setHotWalletContext() {
addParamsInterceptor(HotWalletContextInterceptor())
}
/**
* Erases the context
*/
fun Analytics.eraseContext() {
clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id())
removeParamsInterceptor(HotWalletContextInterceptor.id())
}
/**
* Adds a new context and keeps a previous context as the parent of the new one
*/
fun Analytics.addContext(userWallet: UserWallet) {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = when (userWallet) {
is UserWallet.Cold -> LinkedCardContextInterceptor(userWallet.scanResponse, parent = currentContext)
is UserWallet.Hot -> HotWalletContextInterceptor(parent = currentContext)
}
setUserId(userWalletId = userWallet.walletId.stringValue)
addParamsInterceptor(newContext)
}
/**
@ -48,18 +76,32 @@ fun Analytics.addContext(scanResponse: ScanResponse) {
setUserId(userWalletId.stringValue)
}
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)
addParamsInterceptor(newContext)
}
fun Analytics.addHotWalletContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val newContext = HotWalletContextInterceptor(currentContext)
addParamsInterceptor(newContext)
}
/**
* Removes the current context and restores the previous one if it was present.
*/
fun Analytics.removeContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val previousContext = currentContext?.parent ?: return
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val previousContext = when (currentContext) {
is LinkedCardContextInterceptor -> currentContext.parent
is HotWalletContextInterceptor -> currentContext.parent
else -> null
} ?: return
addParamsInterceptor(previousContext)
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.common.extensions
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
// TODO: move extensions to utils
fun BigDecimal.toFormattedString(
decimals: Int,
roundingMode: RoundingMode = RoundingMode.DOWN,
locale: Locale = Locale.US,
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat()
df.decimalFormatSymbols = symbols
df.maximumFractionDigits = decimals
df.minimumFractionDigits = 0
df.isGroupingUsed = true
df.roundingMode = roundingMode
return df.format(this)
}

View file

@ -2,8 +2,13 @@ package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -11,7 +16,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.text.encodeToByteArray
private const val DEFAULT_KEY = "tangem_pay_default_key"
private const val ORDER_ID_KEY = "tangem_pay_order_id_key"
@ -19,7 +23,9 @@ private const val ORDER_ID_KEY = "tangem_pay_order_id_key"
@Singleton
internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
@NetworkMoshi moshi: Moshi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : TangemPayStorage {
private val secureStorage by lazy {
@ -29,14 +35,21 @@ internal class DefaultTangemPayStorage @Inject constructor(
name = "tangem_pay_storage",
)
}
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
secureStorage.store(key = createCustomerAddressKey(userWalletId), value = customerWalletAddress)
}
}
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
return withContext(dispatcherProvider.io) {
secureStorage.getAsString(createCustomerAddressKey(userWalletId))
}
}
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) =
withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)
@ -67,14 +80,33 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true)
}
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
) == true
}
}
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
}
}
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))
}
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
withContext(dispatcherProvider.io) {
secureStorage.delete(createCustomerAddressKey(userWalletId))
secureStorage.delete(createKey(customerWalletAddress))
secureStorage.delete(createOrderIdKey(customerWalletAddress))
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
}
private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue
private fun createKey(address: String): String = "${DEFAULT_KEY}_$address"

View file

@ -1,14 +1,18 @@
package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -43,17 +47,15 @@ internal object ActivityModule {
@Singleton
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
expressServiceLoader: ExpressServiceLoader,
expressServiceFetcher: ExpressServiceFetcher,
currenciesRepository: CurrenciesRepository,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
sellService = Provider { requireNotNull(appStateHolder.sellService) },
expressServiceLoader = expressServiceLoader,
expressServiceFetcher = expressServiceFetcher,
currenciesRepository = currenciesRepository,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
)
}
@ -63,4 +65,19 @@ internal object ActivityModule {
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
@Provides
@Singleton
fun provideExchangeService(
environmentConfigStorage: EnvironmentConfigStorage,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
moonPayApi: MoonPayApi,
): SellService {
return MoonPayService(
api = moonPayApi,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module
@ -27,6 +28,7 @@ internal class TangemSdkManagerModule {
cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
@ -37,6 +39,7 @@ internal class TangemSdkManagerModule {
resources = context.resources,
visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
)
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import dagger.Module
@ -50,10 +52,12 @@ internal object AccountDomainModule {
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository
@ -24,9 +26,13 @@ internal object NFTDomainModule {
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
@Provides
@ -60,10 +66,12 @@ internal object NFTDomainModule {
@Singleton
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
)
@Provides

View file

@ -179,20 +179,6 @@ internal object OnrampDomainModule {
)
}
@Provides
@Singleton
fun provideGetOnrampV2QuotesUseCase(
settingsRepository: SettingsRepository,
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampV2QuotesUseCase {
return GetOnrampV2QuotesUseCase(
settingsRepository = settingsRepository,
repository = onrampRepository,
errorResolver = onrampErrorResolver,
)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(

View file

@ -20,10 +20,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -59,24 +56,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideFetchPendingTransactionsUseCase(
@ -183,24 +162,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyUseCase(

View file

@ -18,13 +18,17 @@ import com.tangem.core.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -44,6 +48,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
@ -62,6 +67,7 @@ internal class DefaultTangemSdkManager(
private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : TangemSdkManager {
@ -511,6 +517,18 @@ internal class DefaultTangemSdkManager(
)
}
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion
companion object {

View file

@ -213,5 +213,11 @@ class MockTangemSdkManager(
error("Not implemented")
}
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
error("Not implemented")
}
// endregion
}

View file

@ -1,7 +1,11 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.EncryptionMode
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -125,10 +129,18 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
@ -235,6 +247,20 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/1729'/0'/0'") to ExtendedPublicKey( // Tezos
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/111111'/0'/0/0") to ExtendedPublicKey( // Kaspa
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
ByteArrayKey(
@ -244,14 +270,49 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/354'/0'/0'/0'") to ExtendedPublicKey( // Polkadot
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/434'/0'/0'/0'") to ExtendedPublicKey( // Kusama
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/643'/0'/0'/0'") to ExtendedPublicKey( // Azero
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/148'/0'") to ExtendedPublicKey( // Stellar
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -296,9 +357,16 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // xrp
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
DerivationPath("m/44'/1729'/0'/0'") to ExtendedPublicKey( // Tezos
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/111111'/0'/0/0") to ExtendedPublicKey( // Kaspa
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
@ -312,19 +380,54 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/354'/0'/0'/0'") to ExtendedPublicKey( // Polkadot
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/434'/0'/0'/0'") to ExtendedPublicKey( // Kusama
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/643'/0'/0'/0'") to ExtendedPublicKey( // Azero
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/148'/0'") to ExtendedPublicKey( // Stellar
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),

View file

@ -17,7 +17,6 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*
[REDACTED_AUTHOR]
*/
// TODO remove it after test after resolve [REDACTED_JIRA]
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
) : CardSessionRunnable<Boolean> {

View file

@ -0,0 +1,130 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val dispatchersProvider: CoroutineDispatcherProvider,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
) : CardSessionRunnable<TangemPayInitialCredentials> {
override fun run(session: CardSession, callback: CompletionCallback<TangemPayInitialCredentials>) {
coroutineScope.launch {
callback(runSuspend(session = session))
}
}
private suspend fun runSuspend(session: CardSession): CompletionResult<TangemPayInitialCredentials> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
val derivationResult = runDerivationTask(session, wallet)
val address = when (derivationResult) {
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
is CompletionResult.Success<ExtendedPublicKey> -> generateAddressFromExtendedKey(derivationResult.data)
}
val challenge = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address)
}.getOrElse { return CompletionResult.Failure(it.tangemError) }
val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge)
val approveResult = runVisaCustomerWalletApproveTask(
session = session,
cardId = card.cardId,
targetAddress = address,
dataToSign = dataToSign,
)
val signedData = when (approveResult) {
is CompletionResult.Failure<*> -> return CompletionResult.Failure(approveResult.error)
is CompletionResult.Success<VisaSignedDataByCustomerWallet> -> approveResult.data
}
val authTokens = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getTokenWithCustomerWallet(
sessionId = challenge.session.sessionId,
signature = signedData.signature,
nonce = signedData.dataToSign.hashToSign,
)
}.getOrNull() ?: return CompletionResult.Failure(VisaActivationError.FailedRemoteState.tangemError)
return CompletionResult.Success(
data = TangemPayInitialCredentials(
customerWalletAddress = address,
authTokens = authTokens,
),
)
}
private suspend fun runDerivationTask(
session: CardSession,
wallet: CardWallet,
): CompletionResult<ExtendedPublicKey> {
val deferred = CompletableDeferred<CompletionResult<ExtendedPublicKey>>()
val derivationTask = DeriveWalletPublicKeyTask(
walletPublicKey = wallet.publicKey,
derivationPath = VisaUtilities.customDerivationPath,
)
derivationTask.run(session = session, callback = deferred::complete)
return deferred.await()
}
private suspend fun runVisaCustomerWalletApproveTask(
session: CardSession,
cardId: String,
targetAddress: String,
dataToSign: VisaDataToSignByCustomerWallet,
): CompletionResult<VisaSignedDataByCustomerWallet> {
val deferred = CompletableDeferred<CompletionResult<VisaSignedDataByCustomerWallet>>()
val task = VisaCustomerWalletApproveTask(
visaDataForApprove = VisaCustomerWalletApproveTask.Input(
cardId = cardId,
targetAddress = targetAddress,
hashToSign = dataToSign.hashToSign,
sign = dataToSign::sign,
),
)
task.run(session = session, callback = deferred::complete)
return deferred.await()
}
private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String {
val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey(
extendedPublicKey = extendedPublicKey,
cachedIndex = null,
)
return derivationData.address
}
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
@ -59,21 +58,7 @@ class VisaCustomerWalletApproveTask(
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val cardDTO = CardDTO(card)
val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run {
proceedApproveWithLegacyCard(
card = card,
session = session,
callback = callback,
)
return
}
val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run {
callback(CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError))
return
}
val derivationPath = VisaUtilities.customDerivationPath
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))

View file

@ -16,14 +16,11 @@ import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
@ -42,8 +39,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -83,9 +79,9 @@ internal class MainViewModel @Inject constructor(
private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -195,22 +191,12 @@ internal class MainViewModel @Inject constructor(
private fun initializeOffRamp() {
viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService
sellService.update()
}
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
isLogEnabled = LogConfig.network.moonPayService,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

@ -2,44 +2,14 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.utils.converter.Converter
internal class CryptoCurrencyConverter(
private val excludedBlockchains: ExcludedBlockchains,
) : TwoWayConverter<Currency, CryptoCurrency> {
internal object CryptoCurrencyConverter : Converter<CryptoCurrency, Currency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override fun convert(value: Currency): CryptoCurrency {
return when (value) {
is Currency.Blockchain -> requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
is Currency.Token -> requireNotNull(
cryptoCurrencyFactory.createToken(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
}
}
override fun convertBack(value: CryptoCurrency): Currency {
override fun convert(value: CryptoCurrency): Currency {
val blockchain = value.network.toBlockchain()
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
@ -60,15 +30,4 @@ internal class CryptoCurrencyConverter(
)
}
}
fun getSelectedWallet(): UserWallet {
val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
requireNotNull(userWalletsListRepository.selectedUserWallet.value)
} else {
requireNotNull(userWalletListManager.selectedUserWalletSync)
}
}
}

View file

@ -5,13 +5,11 @@ import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.right
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -26,17 +24,13 @@ import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@Suppress("LongParameterList")
internal class DefaultRampManager(
private val sellService: Provider<ExchangeService>,
private val expressServiceLoader: ExpressServiceLoader,
private val sellService: Provider<SellService>,
private val expressServiceFetcher: ExpressServiceFetcher,
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override suspend fun availableForBuy(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
@ -56,7 +50,7 @@ internal class DefaultRampManager(
return either {
val isSellSupportedByService = catch(
block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency)
val serviceCurrency = CryptoCurrencyConverter.convert(status.currency)
sellService().availableForSell(currency = serviceCurrency)
},
@ -96,7 +90,7 @@ internal class DefaultRampManager(
return availabilityState.toReason(cryptoCurrency.name)
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
override fun getSellInitializationStatus(): Flow<SellServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
@ -106,8 +100,8 @@ internal class DefaultRampManager(
}
}
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return expressServiceLoader.getInitializationStatus(userWalletId)
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<SellServiceInitializationStatus> {
return expressServiceFetcher.getInitializationStatus(userWalletId)
}
override suspend fun getSendUnavailabilityReason(
@ -151,14 +145,14 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.exchangeAvailable?.toSwapAvailabilityState()
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.isExchangeAvailable?.toSwapAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
@ -168,15 +162,15 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.onrampAvailable?.toOnrampAvailabilityState()
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.isOnrampAvailable?.toOnrampAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
@ -211,8 +205,13 @@ internal class DefaultRampManager(
}
}
private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean {
val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true)
private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean {
val currencyAssedId = ExpressAsset.ID(
networkId = this.network.backendId,
contractAddress = (this as? CryptoCurrency.Token)?.contractAddress,
)
return assetId.networkId == currencyAssedId.networkId &&
assetId.contractAddress.equals(currencyAssedId.contractAddress, ignoreCase = true)
}
}

View file

@ -5,11 +5,11 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
typealias SellServiceInitializationStatus = Lce<Throwable, Any>
interface ExchangeService {
interface SellService {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
val initializationStatus: StateFlow<SellServiceInitializationStatus>
suspend fun update()

View file

@ -5,7 +5,9 @@ import android.util.Base64
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.moonpay.MoonPayCurrencies
import com.tangem.datasource.api.moonpay.MoonPayUserStatus
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
@ -14,9 +16,10 @@ import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@ -24,25 +27,18 @@ import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val isLogEnabled: Boolean,
private val api: MoonPayApi,
private val apiKeyProvider: Provider<String>,
private val secretKeyProvider: Provider<String>,
private val userWalletProvider: () -> UserWallet?,
) : ExchangeService {
) : SellService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
override val initializationStatus: StateFlow<SellServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
private val _initializationStatus: MutableStateFlow<SellServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MoonPayApi by lazy {
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
logEnabled = isLogEnabled,
).create(MoonPayApi::class.java)
}
private var status: MoonPayStatus? = null
override suspend fun update() {
@ -51,7 +47,7 @@ class MoonPayService(
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
is Result.Failure -> {
Timber.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
@ -60,7 +56,7 @@ class MoonPayService(
is Result.Success -> result.data
}
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
is Result.Failure -> {
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
@ -78,7 +74,7 @@ class MoonPayService(
MoonPayAvailableCurrency(
currencyCode = currency.code,
networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
contractAddress = currency.metadata.contractAddress,
contractAddress = currency.metadata?.contractAddress,
)
}
@ -167,7 +163,7 @@ class MoonPayService(
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("apiKey", apiKeyProvider())
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
@ -191,7 +187,7 @@ class MoonPayService(
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)

View file

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

View file

@ -7,7 +7,7 @@ import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.SellService
import org.rekotlin.Action
import org.rekotlin.Store
import javax.inject.Inject
@ -19,7 +19,7 @@ import javax.inject.Inject
class AppStateHolder @Inject constructor() : ReduxStateHolder {
var mainStore: Store<AppState>? = null
var sellService: ExchangeService? = null
var sellService: SellService? = null
override fun dispatch(action: Action) {
mainStore?.dispatch(action)

View file

@ -36,9 +36,10 @@ import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -62,6 +63,7 @@ internal class ChildFactory @Inject constructor(
private val detailsComponentFactory: DetailsComponent.Factory,
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val walletBackupComponentFactory: WalletBackupComponent.Factory,
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
@ -100,6 +102,7 @@ internal class ChildFactory @Inject constructor(
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createHardwareWalletComponentFactory: CreateHardwareWalletComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -107,8 +110,9 @@ internal class ChildFactory @Inject constructor(
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val viewPhraseComponentFactory: ViewPhraseComponent.Factory,
private val forgetWalletComponentFactory: ForgetWalletComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
@ -138,7 +142,6 @@ internal class ChildFactory @Inject constructor(
is AppRoute.ManageTokens -> {
val source = when (route.source) {
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
}
@ -187,6 +190,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletBackupComponentFactory,
)
}
is AppRoute.WalletHardwareBackup -> {
createComponentChild(
context = context,
params = WalletHardwareBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletHardwareBackupComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
createComponentChild(
context = context,
@ -293,7 +305,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId,
cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId,
),
componentFactory = stakingComponentFactory,
@ -308,6 +320,13 @@ internal class ChildFactory @Inject constructor(
userWalletId = route.userWalletId,
isInitialReverseOrder = route.isInitialReverseOrder,
screenSource = route.screenSource,
tangemPayInput = route.tangemPayInput?.let { tangemPayInput ->
SwapComponent.Params.TangemPayInput(
cryptoAmount = tangemPayInput.cryptoAmount,
fiatAmount = tangemPayInput.fiatAmount,
depositAddress = tangemPayInput.depositAddress,
)
},
),
componentFactory = swapComponentFactory,
)
@ -496,6 +515,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = createWalletSelectionComponentFactory,
)
}
is AppRoute.CreateHardwareWallet -> {
createComponentChild(
context = context,
params = Unit,
componentFactory = createHardwareWalletComponentFactory,
)
}
is AppRoute.CreateMobileWallet -> {
createComponentChild(
context = context,
@ -533,6 +559,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow,
),
componentFactory = createWalletBackupComponentFactory,
)
@ -555,6 +582,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = viewPhraseComponentFactory,
)
}
is AppRoute.ForgetWallet -> {
createComponentChild(
context = context,
params = ForgetWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = forgetWalletComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,
@ -604,8 +640,11 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(config = route.config),
componentFactory = tangemPayDetailsComponentFactory,
params = TangemPayDetailsContainerComponent.Params(
userWalletId = route.userWalletId,
config = route.config,
),
componentFactory = tangemPayDetailsContainerComponentFactory,
)
}
is AppRoute.TangemPayOnboarding -> {

View file

@ -34,18 +34,36 @@ interface Injected {
// Test Logging
subprojects {
tasks.withType<Test> {
tasks.withType<Test>().configureEach {
val taskName = name.lowercase()
if (taskName.contains("external") ||
taskName.contains("internal") ||
taskName.contains("release") ||
taskName.contains("mocked") ||
taskName.contains("huawei")
) {
enabled = false
println("Skipping test task: $name")
} else {
println("Test task scheduled: $name")
}
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showStandardStreams = true
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
if (desc.parent == null) { // will match the outermost suite
val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val output =
"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val startItem = "| "
val endItem = " |"
val repeatLength = startItem.length + output.length + endItem.length
println("\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(repeatLength))
println(
"\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(
repeatLength
)
)
}
}))
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource
@ -126,9 +127,12 @@ sealed class AppRoute(val path: String) : Route {
val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
/**
* Source of launching the screen.
* ManageTokens screen launched from Onboarding by another route. See `OnboardingRoute.ManageTokens`.
*/
enum class Source {
STORIES,
ONBOARDING,
SETTINGS,
}
}
@ -178,13 +182,21 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
val screenSource: String,
val tangemPayInput: TangemPayInput? = null,
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
)
) {
@Serializable
data class TangemPayInput(
val cryptoAmount: SerializedBigDecimal,
val fiatAmount: SerializedBigDecimal,
val depositAddress: String,
)
}
@Serializable
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")
@ -192,9 +204,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Staking(
val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId")
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
@Serializable
data class PushNotification(
@ -217,6 +229,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}")
@Serializable
data class WalletHardwareBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}")
@Serializable
data object Markets : AppRoute(path = "/markets")
@ -322,6 +339,9 @@ sealed class AppRoute(val path: String) : Route {
}
}
@Serializable
object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet")
@Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")
@ -341,6 +361,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class CreateWalletBackup(
val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable
@ -353,6 +374,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}")
@Serializable
data class ForgetWallet(
val userWalletId: UserWalletId,
) : AppRoute(path = "/forget_wallet/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,
@ -383,8 +409,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class TangemPayDetails(
val userWalletId: UserWalletId,
val config: TangemPayDetailsConfig,
) : AppRoute(path = "/tangem_pay_details")
) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}")
@Serializable
data class TangemPayOnboarding(

View file

@ -1,21 +1,30 @@
package com.tangem.common.ui.account
import com.tangem.common.ui.R
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.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.quote.PriceChange
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
class AccountCryptoPortfolioItemStateConverter(
private val appCurrency: AppCurrency,
private val account: Account.CryptoPortfolio,
private val priceChangeLce: Lce<Unit, PriceChange>? = null,
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
) : Converter<TotalFiatBalance, TokenItemState> {
@ -31,6 +40,14 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
val subtitle2State = when (fiatBalance.amount.isZero()) {
true -> null
false -> priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
return TokenItemState.Content(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(this),
@ -50,14 +67,14 @@ class AccountCryptoPortfolioItemStateConverter(
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
isFlickering = fiatBalance.source == StatusSource.CACHE,
),
subtitle2State = null,
subtitle2State = subtitle2State,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
)
}
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Loading {
return TokenItemState.Loading(
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(account),
titleState = TokenItemState.TitleState.Content(
@ -71,6 +88,10 @@ class AccountCryptoPortfolioItemStateConverter(
),
isAvailable = false,
),
fiatAmountState = FiatAmountState.Loading,
subtitle2State = Subtitle2State.Loading,
onItemLongClick = null,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
)
}
@ -97,4 +118,14 @@ class AccountCryptoPortfolioItemStateConverter(
},
)
}
private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this)
private fun StatusSource.isFlickering(): Boolean = this == StatusSource.CACHE
private fun PriceChange.toSubtitle2State(): Subtitle2State = Subtitle2State.PriceChangeContent(
priceChangePercent = this.value.format { percent() },
type = this.value.getPriceChangeType(),
isFlickering = this.source.isFlickering(),
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.account
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import arrow.core.Either
import arrow.core.raise.either
@ -25,7 +26,10 @@ sealed interface AccountNameUM {
*/
data object DefaultMain : AccountNameUM {
override val value: TextReference = resourceReference(R.string.account_main_account_title)
@get:StringRes
val stringResId: Int = R.string.account_main_account_title
override val value: TextReference = resourceReference(stringResId)
}
/**
@ -33,7 +37,7 @@ sealed interface AccountNameUM {
*
* @property raw the raw string value of the custom account name
*/
class Custom(internal val raw: String) : AccountNameUM {
data class Custom(internal val raw: String) : AccountNameUM {
override val value: TextReference = stringReference(value = raw)
}

View file

@ -6,6 +6,7 @@ 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.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
@ -29,6 +30,8 @@ fun AccountTitle(
accountTitleUM: AccountTitleUM,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.subtitle2,
textColor: Color = TangemTheme.colors.text.tertiary,
iconSize: AccountIconSize = AccountIconSize.ExtraSmall,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -40,19 +43,20 @@ fun AccountTitle(
Text(
text = accountTitleUM.prefixText.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
color = textColor,
)
AccountLabel(
name = accountTitleUM.name,
icon = accountTitleUM.icon,
iconSize = AccountIconSize.ExtraSmall,
iconSize = iconSize,
nameStyle = textStyle,
nameColor = textColor,
)
}
is AccountTitleUM.Text -> Text(
text = accountTitleUM.title.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
color = textColor,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
}

View file

@ -0,0 +1,127 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerW12
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.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.AccountName
@Composable
fun PortfolioSelectRow(
state: PortfolioSelectUM,
modifier: Modifier = Modifier,
leftContent: @Composable RowScope.() -> Unit = {},
) {
Row(
modifier = modifier
.clickable(enabled = state.isMultiChoice, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
leftContent()
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(leftText),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW12()
if (state.icon != null) {
AccountIcon(
name = state.name,
icon = state.icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = state.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (state.isMultiChoice) {
Icon(
modifier = Modifier
.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,
val onClick: () -> Unit,
)
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class) state: PortfolioSelectUM) {
TangemThemePreview {
PortfolioSelectRow(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)
}
}
private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(PortfolioSelectRowPreviewData.account, PortfolioSelectRowPreviewData.wallet)
}
object PortfolioSelectRowPreviewData {
val account
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
val wallet
get() = PortfolioSelectUM(
icon = null,
name = stringReference("Wallet Name"),
isMultiChoice = false,
isAccountMode = false,
onClick = {},
)
}

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
@ -47,6 +48,7 @@ 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.SendScreenTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@ -191,7 +193,8 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre
.size(16.dp)
.graphicsLayer {
rotationY = iconRotateState
},
}
.testTag(SendScreenTestTags.EXCHANGE_ICON),
)
AmountFieldCurrencyIcon(
amountUM = amountUM,
@ -227,6 +230,7 @@ private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) {
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT),
)
if (isFiatValue) {
CurrencyIcon(

View file

@ -84,6 +84,7 @@ sealed class AnalyticsParam {
data object LongTap : ScreensSources("Long Tap")
data object Markets : ScreensSources("Markets")
data object HotWallet : ScreensSources("Hot Wallet")
data object TangemPay : ScreensSources("Tangem Pay")
}
sealed class TxSentFrom(val value: String) {

View file

@ -1,6 +1,7 @@
package com.tangem.core.analytics.utils
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
/**
[REDACTED_AUTHOR]
@ -9,9 +10,15 @@ interface AnalyticsContextProxy {
fun setContext(scanResponse: ScanResponse)
fun addContext(userWallet: UserWallet)
fun setHotWalletContext()
fun eraseContext()
fun addContext(scanResponse: ScanResponse)
fun addHotWalletContext()
fun removeContext()
}

View file

@ -30,9 +30,5 @@
{
"name": "zklink",
"version": "undefined"
},
{
"name": "scroll",
"version": "undefined"
}
]

View file

@ -27,6 +27,7 @@ sealed class ApiConfig {
TangemPay,
BlockAid,
YieldSupply,
MoonPay,
}
private fun initializeId(): ID {
@ -37,6 +38,7 @@ sealed class ApiConfig {
is TangemPay -> ID.TangemPay
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* MoonPay [ApiConfig]
*/
internal class MoonPay : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
)
}
}

View file

@ -14,10 +14,12 @@ sealed class ApiResponse<T : Any> {
* Represents a successful response from the API
*
* @property data the data returned by the API
* @property code the HTTP status code of the response
* @property headers the headers returned by the API
*/
data class Success<T : Any>(
val data: T,
val code: ApiResponseError.HttpException.Code = ApiResponseError.HttpException.Code.OK,
override val headers: Map<String, List<String>> = emptyMap(),
) : ApiResponse<T>()
@ -37,11 +39,20 @@ sealed class ApiResponse<T : Any> {
* Wraps data in a [ApiResponse.Success] instance
*
* @param data the data to wrap
* @param code the HTTP status code of the response
* @param headers the headers returned by the API
* @return a [ApiResponse.Success] instance containing the provided data
*/
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
return ApiResponse.Success(data, headers)
internal fun <T : Any> apiSuccess(
data: T,
code: ApiResponseError.HttpException.Code?,
headers: Map<String, List<String>>,
): ApiResponse<T> {
return ApiResponse.Success(
data = data,
code = code ?: ApiResponseError.HttpException.Code.OK,
headers = headers,
)
}
/**

View file

@ -18,8 +18,13 @@ sealed class ApiResponseError : Exception() {
val errorBody: String?,
) : ApiResponseError() {
// TODO: extract Code from HttpException
// region Error Codes
enum class Code(val numericCode: Int) {
// 2xx Success
OK(numericCode = 200),
CREATED(numericCode = 201),
ACCEPTED(numericCode = 202),
// 3xx Server Errors
NOT_MODIFIED(numericCode = 304),
// 4xx Server Errors

View file

@ -15,11 +15,12 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
val headers = headers().toMultimap()
val body = body()
val code = ApiResponseError.HttpException.Code.entries
.firstOrNull { it.numericCode == code() }
return if (isSuccessful && body != null) {
apiSuccess(data = body, headers = headers)
apiSuccess(data = body, code = code, headers = headers)
} else {
val code = ApiResponseError.HttpException.Code.entries
.firstOrNull { it.numericCode == code() }
val e = try {
if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.api.express.models
object TangemExpressValues {
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.network.exchangeServices.moonpay
package com.tangem.datasource.api.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@ -7,17 +7,11 @@ import retrofit2.http.Query
interface MoonPayApi {
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
@GET("v4/ip_address/")
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
@GET("v3/currencies/")
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}
@JsonClass(generateAdapter = true)

View file

@ -7,6 +7,7 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
@ -146,4 +147,10 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
@Body body: CardDetailsRequest,
): ApiResponse<CardDetailsResponse>
@PUT("v1/customer/card/pin")
suspend fun setPin(
@Header("Authorization") authHeader: String,
@Body body: SetPinRequest,
): ApiResponse<SetPinResponse>
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinRequest(
@Json(name = "pin") val pin: String,
@Json(name = "session_id") val sessionId: String,
@Json(name = "iv") val iv: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "result") val result: String,
)
}

View file

@ -137,6 +137,9 @@ interface TangemTechApi {
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
@POST("v1/user-wallets/wallets")
suspend fun createWallet(@Body body: OnlyWalletIdBody): ApiResponse<Unit>
// endregion
// promo

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OnlyWalletIdBody(
@Json(name = "id") val walletId: String,
)

View file

@ -20,4 +20,18 @@ data class GetWalletAccountsResponse(
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}
/** Flattens the tokens from all wallet accounts into a single list */
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group,
sort = wallet.sort,
tokens = flattenTokens(),
)
}

View file

@ -74,4 +74,10 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet
fun provideMoonPayConfig(): ApiConfig {
return MoonPay()
}
}

View file

@ -5,12 +5,14 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.MoonPay
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
@ -130,4 +132,13 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.MoonPay,
applyTimeoutAnnotations = false,
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
}

View file

@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
ApiConfig.ID.MoonPay,
)
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Express service loader
*
[REDACTED_AUTHOR]
*/
interface ExpressServiceLoader {
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
}

View file

@ -171,6 +171,9 @@ object PreferencesKeys {
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
// endregion
}

View file

@ -1,9 +1,13 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
@ -14,5 +18,9 @@ interface TangemPayStorage {
suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String)
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -71,6 +71,7 @@ class ApiConfigTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
ApiConfig.ID.MoonPay -> MoonPay()
}
}
}

View file

@ -109,6 +109,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
ApiConfig.ID.MoonPay -> MoonPay()
}
}
}
@ -121,6 +122,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> createStakeKitModel()
ApiConfig.ID.TangemPay -> createTangemPayModel()
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
ApiConfig.ID.MoonPay -> createMoonPayModel()
}
}
@ -263,6 +265,16 @@ internal class ProdApiConfigsManagerTest {
)
}
private fun createMoonPayModel(): TestModel {
return TestModel(
id = ApiConfig.ID.MoonPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]

View file

@ -194,6 +194,7 @@
<string name="common_fee_selector_title">Velocidad y tarifa</string>
<string name="common_finish">Finalizar</string>
<string name="common_generate_addresses">Sincronizar direcciones</string>
<string name="common_get_started">Comenzar</string>
<string name="common_go_to_provider">Ir al proveedor</string>
<string name="common_go_to_token">Ir al token</string>
<string name="common_got_it">Entendido</string>
@ -984,6 +985,7 @@
<string name="staking_details_account_fee_info">Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking.</string>
<string name="staking_details_annual_percentage_rate">Porcentaje de rendimiento anual</string>
<string name="staking_details_annual_percentage_rate_info">El porcentaje de rendimiento anual que podrá ganar como participante en el staking.</string>
<string name="staking_details_annual_percentage_yield_info">APY muestra el porcentaje de rendimiento anual del validador en función de su rendimiento</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Las recompensas se acumulan automáticamente en su saldo de staking diariamente.</string>
@ -1160,6 +1162,7 @@
<string name="token_button_unavailability_reason_pending_transaction_send">El envío de fondos estará disponible una vez que se complete(n) la(s) transacción(es) pendiente(s) en la red %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">Vender %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones.</string>
<string name="token_button_unavailability_reason_staking_unavailable">El staking de %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones.</string>
<string name="token_button_unavailability_reason_yield_supply_approval">Se ha revocado la aprobación. Sus fondos permanecen en modo de rendimiento. Para realizar acciones, vuelva al modo de rendimiento y otorgue el permiso nuevamente.</string>
<string name="token_details_generate_xpub">Generar XPUB</string>
<string name="token_details_hide_alert_hide">Ocultar</string>
<string name="token_details_hide_alert_message">Está a punto de ocultar este token de la pantalla principal. Puede volver a agregarlo en cualquier momento a través de la página de gestión de tokens.</string>
@ -1549,6 +1552,7 @@
<string name="yield_module_stop_earning_sheet_description">Al desactivar esto, retirará sus fondos de Aave a %s en su billetera y dejará de ganar recompensas.</string>
<string name="yield_module_stop_earning_sheet_fee_note">La comisión de red se deducirá del importe que retire.</string>
<string name="yield_module_stop_earning_sheet_title">Desactivar el modo de rendimiento</string>
<string name="yield_module_supply">Suministrar</string>
<string name="yield_module_supply_apr">Suministro APY</string>
<string name="yield_module_token_details_earn_notification_apy">APY</string>
<string name="yield_module_token_details_earn_notification_description">Los intereses se devengan automáticamente</string>
@ -1560,5 +1564,6 @@
<string name="yield_module_unable_to_cover_fee_description">Deposite algo de %1$s %2$s para cubrir la tarifa de red para las transacciones</string>
<string name="yield_module_unable_to_cover_fee_title">No se puede cubrir la tarifa %s</string>
<string name="yield_module_unavailable_subtitle">El servicio de intereses no está disponible en este momento. Vuelva a intentarlo más tarde.</string>
<string name="yield_module_unavailable_title">Ganancias no disponibles</string>
<string name="yield_module_unavailable_title">Modo de rendimiento no disponible</string>
<string name="yield_supply_chart_loading_error">No se puede cargar el gráfico...</string>
</resources>

View file

@ -172,6 +172,7 @@
<string name="common_fee_selector_title">Vitesse et frais</string>
<string name="common_finish">Terminer</string>
<string name="common_generate_addresses">Synchroniser les adresses</string>
<string name="common_get_started">Commencer</string>
<string name="common_go_to_provider">Aller au fournisseur</string>
<string name="common_go_to_token">Aller au jeton</string>
<string name="common_got_it">Compris</string>
@ -949,6 +950,7 @@
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
<string name="settings_wallet_name_title">Nom</string>
<string name="stake_token_description">Faites travailler votre jeton pour vous</string>
<string name="staking_account_initialization_message">Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking.</string>
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_amount_tron_integer_error_unstaking">Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>

View file

@ -1318,6 +1318,7 @@
<string name="user_wallet_list_rename_popup_title">Переименование кошелька</string>
<string name="user_wallet_list_unlock_all">Разблокировать все</string>
<string name="user_wallet_list_unlock_all_with">Разблокировать все с %s</string>
<string name="visa_onboarding_account_activation_navigation_title">Активация аккаунта</string>
<string name="visa_onboarding_pin_not_accepted">ПИН не принят. Попробуйте ещё раз или введите другой код.</string>
<string name="wallet_add_support_title">Что выбрать?</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуйте позже.</string>

View file

@ -172,6 +172,7 @@
<string name="common_fee_selector_title">Швидкість та комісія</string>
<string name="common_finish">Завершити</string>
<string name="common_generate_addresses">Синхронізувати адреси</string>
<string name="common_get_started">Почати</string>
<string name="common_go_to_provider">Перейти до провайдера</string>
<string name="common_go_to_token">Перейти до токену</string>
<string name="common_got_it">Зrozуміло</string>

View file

@ -969,6 +969,7 @@
</plurals>
<string name="onramp_timing_minutes">%s min</string>
<string name="onramp_title_available_from">Available from</string>
<string name="onramp_title_available_up_to">Available up to</string>
<string name="onramp_title_you_get">You get</string>
<string name="onramp_tos_external_providers">Service is provided by an external provider. \nTangem is not responsible.</string>
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
@ -1370,6 +1371,8 @@
<string name="tangem_pay_status_pending">Pending</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
<string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>
@ -1394,6 +1397,7 @@
<string name="tangempay_card_details_reveal_text">Reveal</string>
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
<string name="tangempay_issue_card_notification_title">Create account and issue a card</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
<string name="tangempay_kyc_in_progress_notification_button">View Status</string>

View file

@ -6,18 +6,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -34,6 +37,7 @@ data class SmallButtonConfig(
val onClick: () -> Unit,
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
val isEnabled: Boolean = true,
val isLoading: Boolean = false,
)
/**
@ -68,7 +72,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
label = "Update background color",
)
Row(
Box(
modifier = modifier
.defaultMinSize(
minWidth = TangemTheme.dimens.size46,
@ -79,7 +83,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
color = backgroundColor,
shape = shape,
)
.clickable(enabled = config.isEnabled, onClick = config.onClick)
.clickable(enabled = !config.isLoading && config.isEnabled, onClick = config.onClick)
.padding(
paddingValues = when (config.icon) {
is TangemButtonIconPosition.None -> PaddingValues(
@ -95,42 +99,54 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
)
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
contentAlignment = Alignment.Center,
) {
ContentContainer(
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = when {
!config.isEnabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
if (config.isLoading) {
CircularProgressIndicator(
strokeWidth = TangemTheme.dimens.size2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.size(TangemTheme.dimens.size16),
)
}
Row(
modifier = Modifier.conditional(config.isLoading) { alpha(0f) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
ContentContainer(
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = when {
!config.isEnabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
Text(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
text = config.text.resolveReference(),
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
},
icon = { iconResId ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = if (config.isEnabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
)
Text(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
text = config.text.resolveReference(),
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
},
icon = { iconResId ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = if (config.isEnabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
)
}
}
}
@ -172,6 +188,7 @@ private fun ButtonsSample() {
)
PrimarySmallButton(config = config)
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add")))
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"), isLoading = true))
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Rating"),
@ -191,5 +208,12 @@ private fun ButtonsSample() {
isEnabled = false,
),
)
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Add token"),
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
isLoading = true,
),
)
}
}

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -133,6 +135,13 @@ fun ActionBaseButton(
}
}
.clip(shape)
.semantics {
contentDescription = if (config.shouldDimContent) {
"Action button is dimmed"
} else {
"Action button is not dimmed"
}
}
.combinedClickable(
enabled = config.isEnabled,
onClick = config.onClick,

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.PopUpMenuTestTags
@ -28,7 +29,7 @@ fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit,
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16)
.testTag(PopUpMenuTestTags.BUTTON),
text = item.title.resolveReference(),
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
style = TangemTheme.typography.button.copy(color = item.textColor.resolveReference()),
)
}
@ -42,7 +43,7 @@ private fun Preview_TokenDetailsAppBarDropdownItem() {
dismissParent = {},
item = TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
textColor = themedColor { TangemTheme.colors.text.warning },
onClick = { },
),
)

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