Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-19 09:16:46 +03:00
commit 6437018fc8
1160 changed files with 35466 additions and 6540 deletions

View file

@ -113,12 +113,14 @@ dependencies {
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk) implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.core) implementation(projects.domain.core)
api(projects.domain.common) api(projects.domain.common)
implementation(projects.domain.card) implementation(projects.domain.card)
implementation(projects.domain.demo) implementation(projects.domain.demo)
implementation(projects.domain.demo.models) implementation(projects.domain.demo.models)
implementation(projects.domain.express)
implementation(projects.domain.wallets) implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.settings) implementation(projects.domain.settings)
@ -160,6 +162,7 @@ dependencies {
implementation(projects.domain.walletManager.models) implementation(projects.domain.walletManager.models)
implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply)
implementation(projects.domain.blockaid) implementation(projects.domain.blockaid)
implementation(projects.domain.hotWallet)
implementation(projects.common) implementation(projects.common)
implementation(projects.common.routing) implementation(projects.common.routing)
@ -167,6 +170,7 @@ dependencies {
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.core.analytics.models) implementation(projects.core.analytics.models)
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.core.abTests)
implementation(projects.core.configToggles) implementation(projects.core.configToggles)
implementation(projects.core.res) implementation(projects.core.res)
implementation(projects.core.ui) implementation(projects.core.ui)
@ -209,6 +213,7 @@ dependencies {
implementation(projects.data.swap) implementation(projects.data.swap)
implementation(projects.data.walletManager) implementation(projects.data.walletManager)
implementation(projects.data.yieldSupply) implementation(projects.data.yieldSupply)
implementation(projects.data.hotWallet)
/** Features */ /** Features */
implementation(projects.features.referral.impl) implementation(projects.features.referral.impl)
@ -245,6 +250,8 @@ dependencies {
implementation(projects.features.walletSettings.impl) implementation(projects.features.walletSettings.impl)
implementation(projects.features.markets.api) implementation(projects.features.markets.api)
implementation(projects.features.markets.impl) implementation(projects.features.markets.impl)
implementation(projects.features.feed.api)
implementation(projects.features.feed.impl)
implementation(projects.features.onramp.api) implementation(projects.features.onramp.api)
implementation(projects.features.onramp.impl) implementation(projects.features.onramp.impl)
implementation(projects.features.onboardingV2.api) implementation(projects.features.onboardingV2.api)

View file

@ -63,7 +63,6 @@ abstract class BaseTestCase : TestCase(
private val apiEnvironmentRule = ApiEnvironmentRule() private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant( private val permissionRule = GrantPermissionRule.grant(
Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.CAMERA,
) )
/** /**

View file

@ -4,8 +4,32 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18" const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0" const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val ETHEREUM_ADDRESS = "0x9FFd974772bDA94D288240C1B22F367Ce75CcD7f"
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3" 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_X_ADDRESS = "XVVzUQZFrRo3MUW3HT1WUaAfE5iNr7USr9ER3DvsKmguJRa"
const val XRP_RECIPIENT_ADDRESS = "rNeY28BPda6jp5N5oESZzd2ZN7eMZy8jNf"
const val XRP_X_RECIPIENT_ADDRESS = "XVNpatGaPNpPbiAJsQz6ZczZFt97QBbFimbtGZnxBMC7ug9"
const val XRP_X_RECIPIENT_ADDRESS_WITH_TAG = "XVNpatGaPNpPbiAJsQz6ZczZFt97QB6jeQWyCM3uCaDHVix"
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 CHIA_RECIPIENT_ADDRESS = "xch1dz2vua2qvxpx6fufd98rxxv2n8j9t3au733xlht3fga4gk8ks5mqq5tzsz"
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 DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
const val WAIT_UNTIL_TIMEOUT = 20_000L const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
@ -14,4 +38,7 @@ object TestConstants {
const val ALLURE_LABEL_NAME = "Owner" const val ALLURE_LABEL_NAME = "Owner"
const val ALLURE_LABEL_VALUE = "Kaspresso" 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( swipeVertical(
direction = SwipeDirection.DOWN, direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f, startHeightRatio = 0.2f,
endHeightRatio = 0.8f, endHeightRatio = 0.8f,
steps = 1000 steps = steps
) )
} }

View file

@ -115,4 +115,28 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Organize tokens' button is displayed") { step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() } 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.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onActionIsUnavailableDialog
import com.tangem.screens.onFailedTransactionDialog import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.Allure.step
@ -63,4 +64,16 @@ fun checkAlreadyUsedWalletDialog() {
step("Assert 'Request support' button is displayed") { step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() } 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,149 @@
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.*
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.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.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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, inputAmount)
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
}
fun BaseTestCase.openSendAddressScreen(
tokenName: String,
inputAmount: 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("Assert 'Send Address' container is displayed") {
onSendAddressScreen { container.assertIsDisplayed() }
}
}
fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) {
step("Assert 'Back' button is displayed") {
onScanQrScreen { backTopAppBarButton.assertIsDisplayed() }
}
step("Assert 'Flashlight' button is displayed") {
onScanQrScreen { flashlightButton.assertIsDisplayed() }
}
step("Assert 'Gallery' button is displayed") {
onScanQrScreen { galleryButton.assertIsDisplayed() }
}
if (!emptyClipboard) {
step("Assert 'Paste from clipboard' is displayed") {
onScanQrScreen { pasteFromClipboardButton.assertIsDisplayed() }
}
}
}
fun BaseTestCase.checkDestinationTagBlock(hint: String) {
step("Assert 'Destination Tag' title is displayed") {
onSendAddressScreen { destinationTagBlockTitle().assertIsDisplayed() }
}
step("Assert 'Destination Tag' text is displayed") {
onSendAddressScreen { destinationTagBlockText.assertIsDisplayed() }
}
step("Assert 'Destination Tag' text field hint contains text '$hint'") {
onSendAddressScreen { destinationTagTextFieldHint.assertTextContains(hint) }
}
step("Assert 'Destination Tag' text field is displayed") {
onSendAddressScreen { destinationTagTextField.assertIsDisplayed() }
}
step("Assert 'Destination Tag' caution is displayed") {
onSendAddressScreen { destinationTagBlockCaution.assertIsDisplayed() }
}
}
fun BaseTestCase.checkRecentAddressItem(address: String, description: String) {
step("Assert recent address '$address' is displayed") {
onSendAddressScreen {
recentAddressItem(address).assertIsDisplayed()
}
}
step("Assert recent address item description is '$description'") {
onSendAddressScreen {
recentAddressItemDescription(description).assertTextContains(
description,
substring = true
)
}
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onSendConfirmScreen
import io.github.kakaocup.compose.node.element.KNode
import io.qameta.allure.kotlin.Allure.step
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

@ -61,20 +61,10 @@ class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProv
useUnmergedTree = true useUnmergedTree = true
} }
val tokenAmountField: KNode = child {
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT))
useUnmergedTree = true
}
val recommendedTitle: KNode = child { val recommendedTitle: KNode = child {
hasText(getResourceString(OnrampImplR.string.onramp_recommended_title)) hasText(getResourceString(OnrampImplR.string.onramp_recommended_title))
} }
val bestRateIcon: KNode = child {
hasTestTag(OnrampOffersBlockTestTags.BEST_RATE_ICON)
useUnmergedTree = true
}
val bestRateTitle: KNode = child { val bestRateTitle: KNode = child {
hasTestTag(OnrampOffersBlockTestTags.BEST_RATE_TITLE) hasTestTag(OnrampOffersBlockTestTags.BEST_RATE_TITLE)
useUnmergedTree = true useUnmergedTree = true

View file

@ -0,0 +1,51 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.feature.qrscanning.impl.R as QrScanningImplR
class CameraAccessDeniedBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CameraAccessDeniedBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(QrScanningImplR.string.qr_scanner_camera_denied_title))
useUnmergedTree = true
}
val subtitle: KNode = child {
hasText(getResourceString(QrScanningImplR.string.qr_scanner_camera_denied_text))
useUnmergedTree = true
}
val settingsButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.qr_scanner_camera_denied_settings_button)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val selectFromGalleryButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.qr_scanner_camera_denied_gallery_button)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
val closeButton: KNode = child {
hasAnyChild(withText(getResourceString(R.string.common_close)))
hasTestTag(BaseBottomSheetTestTags.ACTION_BUTTON)
hasAnyChild(withTestTag(BaseBottomSheetTestTags.ACTION_ICON))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCameraAccessDeniedBottomSheet(function: CameraAccessDeniedBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,14 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.CONTAINER) hasTestTag(BaseDialogTestTags.CONTAINER)
} }
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
}
val cancelButton: KNode = child { val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel)) 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,26 @@
package com.tangem.screens
import com.kaspersky.components.kautomator.component.text.UiButton
import com.kaspersky.components.kautomator.component.text.UiTextView
import com.kaspersky.components.kautomator.screen.UiScreen
object PermissionDialogPageObject : UiScreen<PermissionDialogPageObject>() {
override val packageName = "com.android.permissioncontroller"
val allowWhileUsingButton = UiButton {
withId(this@PermissionDialogPageObject.packageName,"permission_allow_foreground_only_button")
}
val allowOnlyThisTimeButton = UiButton {
withId(this@PermissionDialogPageObject.packageName, "permission_allow_one_time_button")
}
val doNotAllowButton = UiButton {
withId(this@PermissionDialogPageObject.packageName, "permission_deny_button")
}
val permissionMessage = UiTextView {
withId(this@PermissionDialogPageObject.packageName, "permission_message")
}
}

View file

@ -0,0 +1,51 @@
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.QrCodeScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class ScanQrPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ScanQrPageObject>(semanticsProvider = semanticsProvider) {
val backTopAppBarButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val sendTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_send))
useUnmergedTree = true
}
val flashlightButton: KNode = child {
hasTestTag(QrCodeScreenTestTags.FLASHLIGHT_BUTTON)
useUnmergedTree = true
}
val galleryButton: KNode = child {
hasTestTag(QrCodeScreenTestTags.GALLERY_BUTTON)
useUnmergedTree = true
}
fun scanText(tokenName: String): KNode = child {
hasText(getResourceString(R.string.send_qrcode_scan_info, tokenName))
useUnmergedTree = true
}
val pasteFromClipboardButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wallet_connect_paste_from_clipboard))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onScanQrScreen(function: ScanQrPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,21 +3,22 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.*
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletConnectScanQrPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class SellPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectScanQrPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SellPageObject>(semanticsProvider = semanticsProvider) {
val pasteFromClipboardButton: KNode = child { val title: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.wallet_connect_paste_from_clipboard)) hasText(getResourceString(R.string.common_sell))
useUnmergedTree = true useUnmergedTree = true
} }
} }
internal fun BaseTestCase.onWalletConnectScanQrScreen(function: WalletConnectScanQrPageObject.() -> Unit) = internal fun BaseTestCase.onSellScreen(function: SellPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function) onComposeScreen(composeTestRule, function)

View file

@ -9,18 +9,135 @@ import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import 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) : class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val container: KNode = child {
hasTestTag(SendAddressScreenTestTags.CONTAINER)
useUnmergedTree = true
}
val addressTextFieldTitle: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE)
useUnmergedTree = true
}
val addressTextField: KNode = child { val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD) hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true useUnmergedTree = true
} }
val addressPasteButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON)
useUnmergedTree = true
}
val qrButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.QR_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
}
val myWalletsTitle: KNode = child {
hasText(getResourceString(CoreUiR.string.send_recipient_wallets_title))
useUnmergedTree = true
}
fun recentAddressItem(recipientAddress: String, isMyWallet: Boolean = false): KNode = child {
hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM)
hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON))
hasAnyDescendant(withText(recipientAddress))
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
useUnmergedTree = true
if (isMyWallet) {
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recipient_wallets_title)))
hasAnyDescendant(withText(getResourceString(CoreUiR.string.manage_tokens_network_selector_wallet)))
} else {
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recent_transactions)))
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TRANSACTION_ICON))
}
}
fun recentAddressItemDescription(recipientAddress: String): KNode = recentAddressItem(recipientAddress).child {
hasParent(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
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 destinationTagTextFieldHint: KNode = child {
hasParent(withTestTag(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,
CoreUiR.string.send_recipient_memo_footer_v2_highlighted
),
substring = true
)
useUnmergedTree = true
}
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 { val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_next)))
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_next)) hasText(getResourceString(R.string.common_continue))
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -2,36 +2,55 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.test.*
import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.wallet.R
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.test.SendConfirmScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText 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) : class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child { val appBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE) hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true useUnmergedTree = true
} }
val sendButton: KNode = child { val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_send)) hasAnyDescendant(withText(getResourceString(R.string.common_send)))
useUnmergedTree = true 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) hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(CommonUiR.string.send_notification_invalid_amount_title)) hasText(title)
useUnmergedTree = true useUnmergedTree = true
} }
@ -40,34 +59,76 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true useUnmergedTree = true
} }
fun minimumSendAmountErrorIcon(amount: String): KNode = child { fun sendWarningIcon(message: String): KNode = child {
hasAnySibling(
withText(
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
amount,
amount,
)
)
)
hasTestTag(NotificationTestTags.ICON) hasTestTag(NotificationTestTags.ICON)
hasAnySibling(withText(message))
useUnmergedTree = true useUnmergedTree = true
} }
fun minimumSendAmountErrorMessage( fun sendWarningMessage(
amount: String, message: String,
): KNode = child { ): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE) hasTestTag(NotificationTestTags.MESSAGE)
hasText( hasText(message)
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
amount,
amount,
)
)
useUnmergedTree = true 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) = internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =

View file

@ -38,6 +38,16 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true 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 { val tokenName: KNode = child {
hasTestTag(SendScreenTestTags.TOKEN_NAME) hasTestTag(SendScreenTestTags.TOKEN_NAME)
useUnmergedTree = true useUnmergedTree = true
@ -64,6 +74,12 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true useUnmergedTree = true
} }
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_continue))
useUnmergedTree = true
}
} }
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) = 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) @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) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_swap)) hasText(getResourceString(R.string.common_swap))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val sellButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun sellButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_sell)) hasText(getResourceString(R.string.common_sell))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val buyButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun buyButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy)) hasText(getResourceString(R.string.common_buy))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val sendButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun sendButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_send)) 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.BaseTestCase
import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS 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.clickWithAssertion
import com.tangem.common.extensions.pullToRefresh import com.tangem.common.extensions.pullToRefresh
import com.tangem.common.ui.R
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSendScreen
import com.tangem.scenarios.synchronizeAddresses 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 dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test import org.junit.Test
@ -26,28 +35,18 @@ class BlockchainTest : BaseTestCase() {
val validSendAmount = "10" val validSendAmount = "10"
val minAmount = "ADA 1.00" val minAmount = "ADA 1.00"
val address = CARDANO_ADDRESS 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( setupHooks(
additionalAfterSection = { additionalAfterSection = {
resetWireMockScenarioState(scenarioName) resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
} }
).run { ).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { step("Open 'Send Screen' with token: $tokenName") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) openSendScreen(tokenName)
}
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 '$errorSendAmount' in input text field") { step("Type '$errorSendAmount' in input text field") {
onSendScreen { onSendScreen {
@ -64,47 +63,30 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() } onSendAddressScreen { nextButton.clickWithAssertion() }
} }
step("Assert 'Invalid amount' error title is displayed") { step("Assert 'Invalid amount warning' is displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsDisplayed() } checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage,
)
} }
step("Assert 'Invalid amount' error icon is displayed") { step("Click on 'Amount' field") {
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsDisplayed() } onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
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("Type '$validSendAmount' in input text field") { step("Type '$validSendAmount' in input text field") {
onSendScreen { onSendScreen {
amountInputTextField.performClick() amountInputTextField.performClick()
amountInputTextField.performTextReplacement(errorSendAmount) amountInputTextField.performTextReplacement(validSendAmount)
} }
} }
step("Click on 'Next' button") { step("Click on 'Continue' button") {
onSendScreen { nextButton.clickWithAssertion() } onSendScreen { continueButton.clickWithAssertion() }
} }
step("Assert address text field is displayed") { step("Assert 'Invalid amount warning' is not displayed") {
onSendAddressScreen { addressTextField.assertIsDisplayed() } checkSendWarning(
} title = invalidAmountTitle,
step("Click on 'Next' button") { message = invalidAmountMessage,
onSendAddressScreen { nextButton.clickWithAssertion() } isDisplayed = false
} )
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() }
} }
} }
} }
@ -136,10 +118,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
} }
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoErrorState'") { 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'") { step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesErrorState) setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesErrorState
)
} }
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen() openMainScreen()
@ -169,10 +157,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
} }
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoStartedState'") { 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'") { step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesStartedState) setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesStartedState
)
} }
step("Pull to refresh") { step("Pull to refresh") {
pullToRefresh() pullToRefresh()

View file

@ -70,7 +70,7 @@ class BuyTokenTest : BaseTestCase() {
val otherCurrenciesTitle = "Other currencies" val otherCurrenciesTitle = "Other currencies"
val australianDollar = "AUD" val australianDollar = "AUD"
val fiatAmount = "1" val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL" val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods" val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") { step("Reset WireMock scenario '$scenarioName'") {
@ -98,10 +98,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") { step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
} }
step("Assert token amount = '$tokenAmount'") { step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
tokenAmountField.assertTextContains(tokenAmount)
}
} }
step("Fiat currency icon is displayed") { step("Fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() } onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
@ -129,10 +127,8 @@ class BuyTokenTest : BaseTestCase() {
fiatAmountTextField.assertTextContains(australianDollar + fiatAmount) fiatAmountTextField.assertTextContains(australianDollar + fiatAmount)
} }
} }
step("Assert token amount = '$tokenAmount'") { step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
tokenAmountField.assertTextContains(tokenAmount)
}
} }
} }
} }
@ -145,7 +141,7 @@ class BuyTokenTest : BaseTestCase() {
val tokenTitle = "Polygon" val tokenTitle = "Polygon"
val euro = "EUR" val euro = "EUR"
val fiatAmount = "1" val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL" val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods" val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") { step("Reset WireMock scenario '$scenarioName'") {
@ -182,18 +178,12 @@ class BuyTokenTest : BaseTestCase() {
step("Assert fiat amount = '$fiatAmount'") { step("Assert fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) } onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) }
} }
step("Assert token amount = '$tokenAmount'") { step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) } onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
} }
step("Assert 'Recommended' title is displayed") { step("Assert 'Recommended' title is displayed") {
onBuyTokenDetailsScreen { recommendedTitle.clickWithAssertion() } onBuyTokenDetailsScreen { recommendedTitle.clickWithAssertion() }
} }
step("Assert 'Best rate' icon is displayed") {
onBuyTokenDetailsScreen { bestRateIcon.assertIsDisplayed() }
}
step("Assert 'Best rate' title is displayed") { step("Assert 'Best rate' title is displayed") {
onBuyTokenDetailsScreen { bestRateTitle.assertIsDisplayed() } onBuyTokenDetailsScreen { bestRateTitle.assertIsDisplayed() }
} }
@ -316,7 +306,7 @@ class BuyTokenTest : BaseTestCase() {
val paymentMethod = "Invoice Revolut Pay" val paymentMethod = "Invoice Revolut Pay"
val fiatAmount = "1" val fiatAmount = "1"
val providerNameMercuryo = "Mercuryo" val providerNameMercuryo = "Mercuryo"
val tokenAmount = "~488.24938338 POL" val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods" val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") { step("Reset WireMock scenario '$scenarioName'") {
@ -344,11 +334,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") { step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
} }
step("Assert token amount = '$tokenAmount'") { step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) } onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
} }
step("Assert 'Provider block' is displayed") { step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen { providerName.assertIsDisplayed() } onBuyTokenDetailsScreen { providerName.assertIsDisplayed() }
@ -365,9 +352,6 @@ class BuyTokenTest : BaseTestCase() {
step("Assert 'Provider' bottom sheet subtitle is displayed") { step("Assert 'Provider' bottom sheet subtitle is displayed") {
onSelectProviderBottomSheet { subtitle.assertIsDisplayed() } onSelectProviderBottomSheet { subtitle.assertIsDisplayed() }
} }
step("Assert 'Best rate' icon is displayed") {
onSelectProviderBottomSheet { bestRateIcon.assertIsDisplayed() }
}
step("Assert 'Best rate' title is displayed") { step("Assert 'Best rate' title is displayed") {
onSelectProviderBottomSheet { bestRateTitle.assertIsDisplayed() } onSelectProviderBottomSheet { bestRateTitle.assertIsDisplayed() }
} }
@ -406,7 +390,7 @@ class BuyTokenTest : BaseTestCase() {
val invoiceRevolutPay = "Invoice Revolut Pay" val invoiceRevolutPay = "Invoice Revolut Pay"
val sepa = "Sepa" val sepa = "Sepa"
val fiatAmount = "1" val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL" val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods" val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") { step("Reset WireMock scenario '$scenarioName'") {
@ -434,11 +418,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") { step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
} }
step("Assert token amount = '$tokenAmount'") { step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) } onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
} }
step("Assert 'Provider block' is displayed") { step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen { providerName.assertIsDisplayed() } onBuyTokenDetailsScreen { providerName.assertIsDisplayed() }

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

View file

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

View file

@ -1,5 +1,6 @@
package com.tangem.tests package com.tangem.tests
import android.Manifest
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
@ -8,7 +9,7 @@ import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.* import com.tangem.scenarios.*
import com.tangem.screens.onWalletConnectBottomSheet import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScanQrScreen import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectScreen import com.tangem.screens.onWalletConnectScreen
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -196,8 +197,14 @@ class WalletConnectTest : BaseTestCase() {
val dAppName = "React App" val dAppName = "React App"
val context = device.context val context = device.context
val deepLinkUri = getWcUri() val deepLinkUri = getWcUri()
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
setupHooks().run { setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
},
).run {
step("Set URI to clipboard") { step("Set URI to clipboard") {
setClipboardText(context, deepLinkUri) setClipboardText(context, deepLinkUri)
} }
@ -214,7 +221,7 @@ class WalletConnectTest : BaseTestCase() {
onWalletConnectScreen { newConnectionButton.performClick() } onWalletConnectScreen { newConnectionButton.performClick() }
} }
step("CLick 'Paste from clipboard' button") { step("CLick 'Paste from clipboard' button") {
onWalletConnectScanQrScreen { pasteFromClipboardButton.clickWithAssertion() } onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
} }
step("Check 'Wallet Connect' bottom sheet") { step("Check 'Wallet Connect' bottom sheet") {
waitForIdle() waitForIdle()

View file

@ -2,14 +2,19 @@ package com.tangem.tests.actionButtons
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardTextEquals import com.tangem.common.extensions.*
import com.tangem.common.utils.clearClipboard import com.tangem.common.utils.*
import com.tangem.scenarios.openMainScreen import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.scenarios.synchronizeAddresses import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.*
import com.tangem.screens.* 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 dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -121,7 +126,6 @@ class MainScreenActionButtonsTest : BaseTestCase() {
@Test @Test
fun clickOnBuyButtonTest() { fun clickOnBuyButtonTest() {
val tokenTitle = "Bitcoin" val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
setupHooks().run { setupHooks().run {
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
@ -160,12 +164,6 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Assert fiat currency icon is displayed") { step("Assert fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() } onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
} }
step("Assert token amount field is displayed") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenSymbol, substring = true) }
}
step("Assert 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.assertIsDisplayed() }
}
} }
} }
@ -281,41 +279,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Click on 'Receive' button") { step("Click on 'Receive' button") {
onTokenActionsBottomSheet { receiveButton.performClick() } onTokenActionsBottomSheet { receiveButton.performClick() }
} }
step("Assert 'Token receive warning' bottom sheet is displayed") { step("Go to QR code bottom sheet") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
onTokenReceiveWarningBottomSheet { goToQrCodeBottomSheet()
bottomSheet.assertIsDisplayed()
}
} }
} }
step("Click on 'Got it' button") { step("Check QR code bottom sheet") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() } checkQrCodeBottomSheetScenario()
}
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() }
} }
} }
} }
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("85") @AllureId("85")
@DisplayName("Action buttons (long tap): check 'Sell' button") @DisplayName("Action buttons (long tap): check 'Sell' button")
@Test @Test
@ -342,10 +317,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
} }
} }
} }
step("Assert 'Receive' button is displayed") { step("Assert 'Sell' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() } onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
} }
step("Click on 'Receive' button") { step("Click on 'Sell' button") {
onTokenActionsBottomSheet { sellButton.performClick() } onTokenActionsBottomSheet { sellButton.performClick() }
} }
step("Assert Chrome Browser is opened") { step("Assert Chrome Browser is opened") {
@ -391,4 +366,251 @@ 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"
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() }
}
}
}
@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,137 @@
package com.tangem.tests.send.addressScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.DOGECOIN_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSendAddressScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
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 MyWalletsBlockTest : BaseTestCase() {
private val txHistoryScenarioName = "dogecoin_tx_history"
@AllureId("4579")
@DisplayName("Send (address screen): click on 'My wallet address'")
@Test
fun clickOnMyWalletAddressTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "OutgoingTransaction"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Click on 'My wallet' address") {
onSendAddressScreen {
recentAddressItem(recipientAddress = DOGECOIN_ADDRESS, isMyWallet = true).performClick()
}
}
step("Assert recipient address is displayed on confirm screen") {
onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() }
}
}
}
@AllureId("4599")
@DisplayName("Send (address screen): 'My wallets' block is not displayed for account networks")
@Test
fun myWalletsBlockForAccountNetworksNotDisplayedTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'My wallets' title is not displayed") {
onSendAddressScreen { myWalletsTitle.assertIsNotDisplayed() }
}
step("Assert recent address item is not displayed") {
onSendAddressScreen { recentAddressItem(ETHEREUM_ADDRESS).assertIsNotDisplayed() }
}
}
}
@AllureId("4737")
@DisplayName("Send (address screen): assert 'My wallet address is correct'")
@Test
fun checkMyWalletsBlockTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "OutgoingTransaction"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'My wallets' title is displayed") {
onSendAddressScreen { myWalletsTitle.assertIsDisplayed() }
}
step("Assert 'My wallet' address is displayed") {
onSendAddressScreen {
recentAddressItem(recipientAddress = DOGECOIN_ADDRESS, isMyWallet = true).performClick()
}
}
}
}
}

View file

@ -0,0 +1,330 @@
package com.tangem.tests.send.addressScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.DOGECOIN_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.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.*
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
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 RecentBlockTest : BaseTestCase() {
private val txHistoryScenarioName = "dogecoin_tx_history"
private val descriptionForWalletTransaction = "DOGE 0.0192, 11/10/2025, 1:46 PM"
@AllureId("4005")
@DisplayName("Send (address screen): check address history")
@Test
fun sendAddressHistoryTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "OutgoingTransaction"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Click on previous address") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() }
}
step("Assert recipient address is displayed") {
onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() }
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert address text field contains correct recipient address") {
onSendAddressScreen { addressTextField.assertTextContains(DOGECOIN_ADDRESS) }
}
step("Assert 'Recent' title is not 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("4597")
@DisplayName("Send (address screen): check 'Recent' block, transaction history doesn't supported")
@Test
fun recentBlockTransactionHistoryDoesNotSupportedTest() {
val tokenName = "Polkadot"
val sendAmount = "1"
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("Assert 'Send Address' container is displayed") {
onSendAddressScreen { container.assertIsDisplayed() }
}
step("Assert 'Recent' title is not displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
}
}
@AllureId("4596")
@DisplayName("Send (address screen): check 'Recent' block, transaction history is empty")
@Test
fun recentBlockTransactionHistoryEmptyTest() {
val tokenName = "POL (ex-MATIC)"
val sendAmount = "1"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'Recent' title is not displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
step("Assert recent address item is not displayed") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).assertIsNotDisplayed() }
}
}
}
@AllureId("4596")
@DisplayName("Send (address screen): check 'Recent' block, transaction history does not load")
@Test
fun recentBlockTransactionHistoryDoesNotLoadTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "Error"
val scenarioState = "Dogecoin"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
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("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'Recent' title is not displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
step("Assert recent address item is not displayed") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).assertIsNotDisplayed() }
}
}
}
@AllureId("4572")
@DisplayName("Send (address screen): check 'Recent' block for >10 transactions")
@Test
fun checkRecentBlockMoreThanTenTransactionsTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "11OutgoingTransactions"
val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq"
val shortenedRecipientAddress = "DJ2TaZ5vvp3mBLugU...Li4uYaq123456789b"
val descriptionForTransaction = "DOGE 0.10, 11/10/2025, 1:46 PM"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() }
}
step("Check recent address item №1") {
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction)
}
step("Check recent address item №2") {
checkRecentAddressItem(address = shortenedRecipientAddress, description = descriptionForTransaction)
}
step("Check recent address item №3") {
checkRecentAddressItem(address = recipientAddressBase + "k", description = descriptionForTransaction)
}
step("Check recent address item №4") {
checkRecentAddressItem(address = recipientAddressBase + "c", description = descriptionForTransaction)
}
step("Check recent address item №5") {
checkRecentAddressItem(address = recipientAddressBase + "d", description = descriptionForTransaction)
}
step("Check recent address item №6") {
checkRecentAddressItem(address = recipientAddressBase + "e", description = descriptionForTransaction)
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Check recent address item №7") {
checkRecentAddressItem(address = recipientAddressBase + "f", description = descriptionForTransaction)
}
step("Check recent address item №8") {
checkRecentAddressItem(address = recipientAddressBase + "g", description = descriptionForTransaction)
}
step("Check recent address item №9") {
checkRecentAddressItem(address = recipientAddressBase + "o", description = descriptionForTransaction)
}
step("Check recent address item №10") {
checkRecentAddressItem(address = recipientAddressBase + "p", description = descriptionForTransaction)
}
step("Assert recent address item №11 is not displayed") {
onSendAddressScreen { recentAddressItem(recipientAddressBase + "a").assertIsNotDisplayed() }
}
step("Click on recent address item №1") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() }
}
step("Assert recipient address is displayed on 'Send Confirm' screen") {
onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() }
}
}
}
@AllureId("4676")
@DisplayName("Send (address screen): check 'Recent' block for <10 transactions")
@Test
fun checkRecentBlockLessThanTenTransactionsTest() {
val tokenName = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "2OutgoingTransactions"
val descriptionForWalletTransaction2 = "DOGE 0.0192, 11/09/2025, 3:18 PM"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(txHistoryScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() }
}
step("Check recent address item") {
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction)
}
step("Check recent address item") {
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction2)
}
step("Click on recent address item") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() }
}
step("Assert recipient address is displayed on 'Send Confirm' screen") {
onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() }
}
}
}
}

View file

@ -0,0 +1,583 @@
package com.tangem.tests.send.addressScreen
import android.Manifest
import androidx.compose.ui.test.hasText
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.XRP_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_X_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS_WITH_TAG
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setClipboardText
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.wallet.BuildConfig
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("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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
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) }
}
}
}
@AllureId("4574")
@DisplayName("Send (address screen): check scan QR code screen")
@Test
fun checkQrCodeScreenTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Click on 'QR' button") {
onSendAddressScreen { qrButton.clickWithAssertion() }
}
step("Check 'Scan QR code' screen") {
checkScanQrScreen()
}
}
}
@AllureId("4576")
@DisplayName("Send (address screen): give camera permission")
@Test
fun giveCameraPermissionScreenTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Click on 'QR' button") {
onSendAddressScreen { qrButton.clickWithAssertion() }
}
step("Assert 'While using app' button is displayed") {
waitForIdle()
PermissionDialogPageObject { allowWhileUsingButton.isDisplayed() }
}
step("Assert 'Allow only this time' button is displayed") {
PermissionDialogPageObject { allowOnlyThisTimeButton.isDisplayed() }
}
step("Assert 'Don't allow' button is displayed") {
PermissionDialogPageObject { doNotAllowButton.isDisplayed() }
}
step("CLick on 'While using app' button") {
PermissionDialogPageObject { allowWhileUsingButton.click() }
}
step("Check 'Scan QR code' screen") {
checkScanQrScreen()
}
}
}
@AllureId("4578")
@DisplayName("Send (address screen): don't give camera permission")
@Test
fun doNotGiveCameraPermissionScreenTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Click on 'QR' button") {
onSendAddressScreen { qrButton.clickWithAssertion() }
}
step("Click on 'Don't allow' button") {
waitForIdle()
PermissionDialogPageObject { doNotAllowButton.click() }
}
step("Assert 'Camera access denied' bottom sheet title is displayed") {
waitForIdle()
onCameraAccessDeniedBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Camera access denied' bottom sheet subtitle is displayed") {
onCameraAccessDeniedBottomSheet { subtitle.assertIsDisplayed() }
}
step("Assert 'Settings' button is displayed") {
onCameraAccessDeniedBottomSheet { settingsButton.assertIsDisplayed() }
}
step("Assert 'Select from the gallery' button is displayed") {
onCameraAccessDeniedBottomSheet { selectFromGalleryButton.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onCameraAccessDeniedBottomSheet { closeButton.assertIsDisplayed() }
}
step("Click on 'Close' button") {
onCameraAccessDeniedBottomSheet { closeButton.performClick() }
}
step("Assert 'Address' screen is displayed") {
onSendAddressScreen { container.assertIsDisplayed() }
}
}
}
@AllureId("4590")
@DisplayName("Send (address screen): enter 'r' address (XRP)")
@Test
fun xrpEnterRAddressTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val xrpRecipientAddress = XRP_RECIPIENT_ADDRESS
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val optionalText = getResourceString(R.string.send_optional_field)
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Check 'Destination tag' block") {
checkDestinationTagBlock(hint = optionalText)
}
}
}
@AllureId("4591")
@DisplayName("Send (address screen): enter 'X' address without tag (XRP)")
@Test
fun xrpEnterXAddressWithoutTagTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included)
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Check 'Destination tag' block") {
checkDestinationTagBlock(hint = tagAlreadyIncluded)
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
}
}
@AllureId("4592")
@DisplayName("Send (address screen): enter 'X' address with tag (XRP)")
@Test
fun xrpEnterXAddressWithTagTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS_WITH_TAG
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included)
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Check 'Destination tag' block") {
checkDestinationTagBlock(hint = tagAlreadyIncluded)
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
}
}
@AllureId("4594")
@DisplayName("Send (address screen): enter own address (XRP)")
@Test
fun xrpEnterOwnXAddressTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val walletAddress = XRP_X_ADDRESS
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val sameAsWalletAddress = getResourceString(R.string.send_error_address_same_as_wallet)
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(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("4593")
@DisplayName("Send (address screen): enter tag before address (XRP)")
@Test
fun xrpEnterTagBeforeAddressTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS
val destinationTag = "12345"
val emptyText = ""
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included)
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("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Type destination tag in input text field") {
onSendAddressScreen { destinationTagTextField.performTextReplacement(destinationTag) }
}
step("Type X address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Destination tag text field doesn't contains destination tag: '$destinationTag'") {
onSendAddressScreen { destinationTagTextField.assert (!hasText(destinationTag)) }
}
step("Destination tag text field cleared") {
onSendAddressScreen { destinationTagTextField.assertTextEquals(emptyText) }
}
step("Check 'Destination tag' block") {
checkDestinationTagBlock(hint = tagAlreadyIncluded)
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
}
}
}

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,162 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.CHIA_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.common.utils.setWireMockScenarioState
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.Ignore
import org.junit.Test
@HiltAndroidTest
@Ignore("[REDACTED_JIRA]")
class ChiaWarningsTest : BaseTestCase() {
private val tokenName = "Chia Network"
private val mockStateName = "Chia"
private val amountToSendUsingLessThanUTXOLimit = "20"
private val amountToSendUsingMoreThanUTXOLimit = "51"
private val amountToSendUsingExactlyUTXOLimit = "50"
private val utxoLimitAmount = "50"
private val chiaUTXOScenarioName = "chia_get_coin_records"
private val moreThanLimitState = "more_than_50_android"
private val warningTitle = getResourceString(R.string.send_notification_transaction_limit_title)
private val warningMessage = getResourceString(
R.string.send_notification_transaction_limit_text,
tokenName, utxoLimitAmount, utxoLimitAmount
)
@AllureId("4226")
@DisplayName("Warnings: check warning, when sending less than 50 utxo")
@Test
fun checkWarningWhenSendingLessThanUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$chiaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = chiaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$amountToSendUsingLessThanUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingLessThanUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CHIA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4227")
@DisplayName("Warnings: check warning is not displayed, when sending exactly 50 utxo")
@Test
fun checkWarningWhenSendingEqualToUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$chiaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = chiaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$amountToSendUsingExactlyUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingExactlyUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CHIA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is not displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4228")
@DisplayName("Warnings: check warning is not displayed, when sending more than 50 utxo")
@Test
fun checkWarningWhenSendingMoreThanUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$chiaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = chiaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName, mockStateName)
}
step("Type '$amountToSendUsingMoreThanUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingMoreThanUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(CHIA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage
)
}
}
}
}

View file

@ -0,0 +1,183 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.DOGECOIN_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.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 DogecoinWarningsTest : BaseTestCase() {
private val tokenName = "Dogecoin"
private val amountToLeaveLessThanDust = "5.78654978"
private val amountToLeaveMoreThanDust = "5.7"
private val amountGreaterThanDust = "0.02"
private val amountLessThanDust = "0.005"
private val dustAmount = "DOGE 0.01"
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
private val invalidAmountMessage = getResourceString(
R.string.send_notification_invalid_minimum_amount_text,
dustAmount, dustAmount
)
@AllureId("4213")
@DisplayName("Warnings: warning is displayed, if after send balance is less than dust amount (Dogecoin)")
@Test
fun warningIsDisplayedWhenLeaveLessThanDust() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDust' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDust)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage
)
}
}
}
@AllureId("4214")
@DisplayName("Warnings: warning is NOT displayed, if after send balance is more than dust amount (Dogecoin)")
@Test
fun warningIsNotDisplayedWhenLeaveMoreThanDust() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveMoreThanDust' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveMoreThanDust)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_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("4211")
@DisplayName("Warnings: warning is NOT displayed, when sending more than dust amount (Dogecoin)")
@Test
fun warningIsNotDisplayedWhenSendingMoreThanDust() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountGreaterThanDust' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountGreaterThanDust)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_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("4212")
@DisplayName("Warnings: warning is displayed, when sending less than dust amount (Dogecoin)")
@Test
fun warningIsDisplayedWhenSendingLessThanDust() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountLessThanDust' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountLessThanDust)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
title = invalidAmountTitle,
message = invalidAmountMessage
)
}
}
}
}

View file

@ -0,0 +1,160 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.KASPA_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.common.utils.setWireMockScenarioState
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 KaspaWarningsTest : BaseTestCase() {
private val tokenName = "Kaspa"
private val amountToSendUsingLessThanUTXOLimit = "8.0"
private val amountToSendUsingMoreThanUTXOLimit = "8.7"
private val amountToSendUsingExactlyUTXOLimit = "8.19968"
private val utxoLimitAmount = "84"
private val availableToSendAmount = "KAS 8.19968"
private val kaspaUTXOScenarioName = "kaspa_utxo"
private val moreThanLimitState = "more_than_84_android"
private val warningTitle = getResourceString(R.string.send_notification_transaction_limit_title)
private val warningMessage = getResourceString(
R.string.send_notification_transaction_limit_text,
tokenName, utxoLimitAmount, availableToSendAmount
)
@AllureId("4223")
@DisplayName("Warnings: check kaspa utxo warning, when sending less than 84 utxo")
@Test
fun checkWarningWhenSendingLessThanUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToSendUsingLessThanUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingLessThanUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is NOT displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage,
isDisplayed = false
)
}
}
}
@AllureId("4225")
@DisplayName("Warnings: check kaspa utxo warning, when sending more than 84 utxo")
@Test
fun checkWarningWhenSendingMoreThanUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToSendUsingMoreThanUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingMoreThanUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is displayed") {
checkSendWarning(
title = warningTitle,
message = warningMessage
)
}
}
}
@AllureId("4224")
@DisplayName("Warnings: check kaspa utxo warning, when sending exactly 84 utxo")
@Test
fun checkWarningWhenSendingEqualToUTXOLimit() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$moreThanLimitState'") {
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = moreThanLimitState)
}
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToSendUsingExactlyUTXOLimit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToSendUsingExactlyUTXOLimit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'UTXO limit warning' is 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> </intent-filter>
</service> </service>
</application> </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> </manifest>

View file

@ -5,7 +5,9 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.GlobalUiMessageSender
@ -151,4 +153,8 @@ interface ApplicationEntryPoint {
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase fun getWcInitializeUseCase(): WcInitializeUseCase
fun getTrackingContextProxy(): TrackingContextProxy
fun getABTestsManager(): ABTestsManager
} }

View file

@ -302,7 +302,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> { private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
val tangemApplication = application as TangemApplication val tangemApplication = requireNotNull(application as? TangemApplication) {
"Application is null"
}
return tangemApplication.getAppThemeModeUseCase() return tangemApplication.getAppThemeModeUseCase()
.filterNotNull() .filterNotNull()

View file

@ -16,6 +16,7 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -240,6 +241,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val wcInitializeUseCase private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase() get() = entryPoint.getWcInitializeUseCase()
private val trackingContextProxy
get() = entryPoint.getTrackingContextProxy()
private val abTestsManager: ABTestsManager
get() = entryPoint.getABTestsManager()
// endregion // endregion
private val appScope = MainScope() private val appScope = MainScope()
@ -310,6 +317,8 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
} }
abTestsManager.init()
appScope.launch { appScope.launch {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
loadNativeLibraries() loadNativeLibraries()
@ -385,6 +394,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
userWalletsListRepository = userWalletsListRepository, userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk, tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles, hotWalletFeatureToggles = hotWalletFeatureToggles,
trackingContextProxy = trackingContextProxy,
), ),
), ),
) )

View file

@ -1,31 +0,0 @@
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.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
/**
[REDACTED_AUTHOR]
*/
internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
override fun setContext(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
override fun eraseContext() {
Analytics.eraseContext()
}
override fun addContext(scanResponse: ScanResponse) {
Analytics.addContext(scanResponse)
}
override fun removeContext() {
Analytics.removeContext()
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.setContext
internal class DefaultChangeCardAnalyticsContextUseCase : ChangeCardAnalyticsContextUseCase {
override fun invoke(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.tap.common.analytics
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.TrackingContextProxy
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.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
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.domain.models.wallet.UserWalletId
/**
[REDACTED_AUTHOR]
*/
internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsManager) : TrackingContextProxy {
override fun setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
Analytics.setContext(userWalletId, scanResponse)
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWalletId),
batch = scanResponse.card.batchId,
productType = scanResponse.productType.name,
firmware = scanResponse.card.firmwareVersion.stringValue,
)
}
override fun setContext(userWallet: UserWallet) {
Analytics.setContext(userWallet)
when (userWallet) {
is UserWallet.Cold -> {
setColdWalletUserProperties(userWallet)
}
is UserWallet.Hot -> {
setHotWalletUserProperties(userWallet)
}
}
}
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() {
Analytics.eraseContext()
abTestsManager.removeUserProperties()
}
override fun addContext(scanResponse: ScanResponse) {
Analytics.addContext(scanResponse)
}
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() {
Analytics.removeContext()
}
private fun calculateUserIdHash(userWalletId: UserWalletId?): String? {
return userWalletId?.value
?.calculateSha256()
?.toHexString()
}
private fun setColdWalletUserProperties(userWallet: UserWallet.Cold) {
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWallet.walletId),
batch = userWallet.scanResponse.card.batchId,
productType = userWallet.scanResponse.productType.name,
firmware = userWallet.scanResponse.card.firmwareVersion.stringValue,
)
}
private fun setHotWalletUserProperties(userWallet: UserWallet.Hot) {
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWallet.walletId),
batch = null,
productType = "Mobile Wallet",
firmware = null,
)
}
}

View file

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

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.handlers package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.ExceptionHandlerOutput import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent
import javax.inject.Inject import javax.inject.Inject
@ -8,12 +10,13 @@ import javax.inject.Inject
class BlockchainExceptionHandler @Inject constructor( class BlockchainExceptionHandler @Inject constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler, private val analyticsErrorHandler: AnalyticsErrorHandler,
) : ExceptionHandlerOutput { ) : ExceptionHandlerOutput {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { override fun handleApiSwitch(currentHost: String, nextHost: String, message: String, blockchain: Blockchain) {
analyticsErrorHandler.sendErrorEvent( analyticsErrorHandler.sendErrorEvent(
BlockchainApiExceptionEvent( BlockchainApiExceptionEvent(
selectedHost = nextHost, selectedHost = nextHost,
exceptionHost = currentHost, exceptionHost = currentHost,
error = message, 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( class LinkedCardContextInterceptor(
scanResponse: ScanResponse, scanResponse: ScanResponse,
val parent: LinkedCardContextInterceptor? = null, val parent: ParamsInterceptor? = null,
) : ParamsInterceptor { ) : ParamsInterceptor {
private val contextInterceptor = CardContextInterceptor(scanResponse) private val contextInterceptor = CardContextInterceptor(scanResponse)

View file

@ -3,7 +3,9 @@ package com.tangem.tap.common.extensions
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/** /**
@ -13,8 +15,7 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc
/** /**
* Sets the new context * Sets the new context
*/ */
fun Analytics.setContext(scanResponse: ScanResponse) { fun Analytics.setContext(userWalletId: UserWalletId?, scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId != null) { if (userWalletId != null) {
setUserId(userWalletId.stringValue) setUserId(userWalletId.stringValue)
} }
@ -24,19 +25,46 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) { fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue) setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) { when (userWallet) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) 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 * Erases the context
*/ */
fun Analytics.eraseContext() { fun Analytics.eraseContext() {
clearUserId() clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id()) 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(userId = userWallet.walletId.stringValue)
addParamsInterceptor(newContext)
} }
/** /**
@ -48,18 +76,32 @@ fun Analytics.addContext(scanResponse: ScanResponse) {
setUserId(userWalletId.stringValue) setUserId(userWalletId.stringValue)
} }
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)
addParamsInterceptor(newContext) 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. * Removes the current context and restores the previous one if it was present.
*/ */
fun Analytics.removeContext() { fun Analytics.removeContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
val previousContext = currentContext?.parent ?: return ?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val previousContext = when (currentContext) {
is LinkedCardContextInterceptor -> currentContext.parent
is HotWalletContextInterceptor -> currentContext.parent
else -> null
} ?: return
addParamsInterceptor(previousContext) 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

@ -54,8 +54,8 @@ private class CoilTimberLogger : Logger {
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) { override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
with(Timber.tag(COIL_LOG_TAG)) { with(Timber.tag(COIL_LOG_TAG)) {
throwable?.let { e -> e(e, message) } if (throwable != null) e(throwable, message)
message?.let { msg -> d(msg) } if (message != null) d(message)
} }
} }
} }

View file

@ -54,8 +54,8 @@ class PushNotificationDelegate(private val context: Context) {
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setVibrate(vibratePattern) .setVibrate(vibratePattern)
.apply { .apply {
imageUrl?.let { uri -> if (imageUrl != null) {
val bitmap = getBitmapImageFromUrl(uri) val bitmap = getBitmapImageFromUrl(imageUrl)
setStyle( setStyle(
NotificationCompat NotificationCompat
.BigPictureStyle() .BigPictureStyle()
@ -64,7 +64,10 @@ class PushNotificationDelegate(private val context: Context) {
} }
} }
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val service = context.getSystemService(Context.NOTIFICATION_SERVICE)
val notificationManager = requireNotNull(service as? NotificationManager) {
"NotificationManager not available"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel( val notificationChannel = NotificationChannel(

View file

@ -6,6 +6,7 @@ import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action import org.rekotlin.Action
@Suppress("CanBeNonNullable")
fun appReducer(action: Action, state: AppState?): AppState { fun appReducer(action: Action, state: AppState?): AppState {
requireNotNull(state) requireNotNull(state)
if (action is AppAction.RestoreState) return action.state if (action is AppAction.RestoreState) return action.state

View file

@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.rekotlin.Middleware import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
internal object LegacyMiddleware { internal object LegacyMiddleware {
private val prepareDetailsScreenJobHolder = JobHolder() private val prepareDetailsScreenJobHolder = JobHolder()
@ -29,7 +30,14 @@ internal object LegacyMiddleware {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository) val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
selectedUserWallet() selectedUserWallet()
.distinctUntilChanged() .distinctUntilChanged { old, new ->
if (old is UserWallet.Cold && new is UserWallet.Cold) {
old.walletId == new.walletId &&
old.scanResponse == new.scanResponse
} else {
old.walletId == new.walletId
}
}
.onEach { selectedUserWallet -> .onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState( val initializedAppSettingsStateContent = initializeAppSettingsState(
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(), shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
@ -78,6 +86,7 @@ internal object LegacyMiddleware {
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings, .getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
hasSecuredWallets = store.inject(DaggerGraphState::userWalletsListRepository).hasSecuredWallets(),
) )
} }
} }

View file

@ -41,8 +41,8 @@ object SimpleCancelableAlertDialog {
context: Context, context: Context,
): AlertDialog { ): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(titleRes?.let { context.getString(it) } ?: title) setTitle(if (titleRes != null) context.getString(titleRes) else title)
setMessage(messageRes?.let { context.getString(it) } ?: message) setMessage(if (messageRes != null) context.getString(messageRes) else message)
setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() } setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() }
if (secondaryButtonRes != null) { if (secondaryButtonRes != null) {
setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction() } setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction() }

View file

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

View file

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

@ -4,9 +4,9 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import com.tangem.tap.common.haptic.DefaultVibratorHapticManager
import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.tap.common.haptic.DefaultVibratorHapticManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -22,10 +22,14 @@ class HapticModule {
@Singleton @Singleton
fun provideHapticManager(@ApplicationContext context: Context): VibratorHapticManager { fun provideHapticManager(@ApplicationContext context: Context): VibratorHapticManager {
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager val service = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE)
val vibratorManager = requireNotNull(service as? VibratorManager) {
"VibratorManager not available"
}
vibratorManager.defaultVibrator vibratorManager.defaultVibrator
} else { } else {
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator val service = context.getSystemService(Context.VIBRATOR_SERVICE)
requireNotNull(service as? Vibrator) { "Vibrator service not available" }
} }
return if (vibrator.hasVibrator()) { return if (vibrator.hasVibrator()) {

View file

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

View file

@ -1,10 +1,9 @@
package com.tangem.tap.di.analytics package com.tangem.tap.di.analytics
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.tap.common.analytics.DefaultTrackingContextProxy
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -18,13 +17,8 @@ internal object AnalyticsModule {
@Provides @Provides
@Singleton @Singleton
fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase { fun provideAnalyticsContextProxy(abtestsManager: ABTestsManager): TrackingContextProxy =
return DefaultChangeCardAnalyticsContextUseCase() DefaultTrackingContextProxy(abtestsManager)
}
@Provides
@Singleton
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
@Provides @Provides
@Singleton @Singleton

View file

@ -3,8 +3,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository 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.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.* import com.tangem.domain.account.usecase.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -50,10 +53,12 @@ internal object AccountDomainModule {
fun provideRecoverCryptoPortfolioUseCase( fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration, mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
): RecoverCryptoPortfolioUseCase { ): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase( return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository, crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration, mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
) )
} }
@ -82,4 +87,16 @@ internal object AccountDomainModule {
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
) )
} }
@Provides
@Singleton
fun provideApplyAccountListSortingUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
dispatchers: CoroutineDispatcherProvider,
): ApplyAccountListSortingUseCase {
return ApplyAccountListSortingUseCase(
accountsCRUDRepository = accountsCRUDRepository,
dispatchers = dispatchers,
)
}
} }

View file

@ -33,7 +33,7 @@ internal object CardDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig)
@Provides @Provides
@Singleton @Singleton

View file

@ -0,0 +1,27 @@
package com.tangem.tap.di.domain
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object HotWalletDomainModule {
@Provides
@Singleton
fun provideGetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): GetAccessCodeSkippedUseCase {
return GetAccessCodeSkippedUseCase(hotWalletRepository)
}
@Provides
@Singleton
fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase {
return SetAccessCodeSkippedUseCase(hotWalletRepository)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di.domain 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.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.* import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.nft.repository.NFTRepository
@ -24,9 +26,13 @@ internal object NFTDomainModule {
fun providesGetNFTCollectionsUseCase( fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
nftRepository = nftRepository, nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
) )
@Provides @Provides
@ -60,10 +66,12 @@ internal object NFTDomainModule {
@Singleton @Singleton
fun providesGetNFTAvailableNetworksUseCase( fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase( ): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
nftRepository = nftRepository, nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
) )
@Provides @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 @Provides
@Singleton @Singleton
fun provideGetOnrampProviderWithQuoteUseCase( 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.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.*
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.wallet.WalletBalanceFetcher import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles 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 @Provides
@Singleton @Singleton
fun provideFetchPendingTransactionsUseCase( 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 @Provides
@Singleton @Singleton
fun provideGetCryptoCurrencyUseCase( fun provideGetCryptoCurrencyUseCase(

View file

@ -28,7 +28,7 @@ internal object TransactionDomainModule {
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
return GetFeeUseCase( return GetFeeUseCase(
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(), demoConfig = DemoConfig,
) )
} }
@ -48,7 +48,7 @@ internal object TransactionDomainModule {
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SendTransactionUseCase { ): SendTransactionUseCase {
return SendTransactionUseCase( return SendTransactionUseCase(
demoConfig = DemoConfig(), demoConfig = DemoConfig,
cardSdkConfigRepository = cardSdkConfigRepository, cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository, transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
@ -126,7 +126,7 @@ internal object TransactionDomainModule {
fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase {
return EstimateFeeUseCase( return EstimateFeeUseCase(
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(), demoConfig = DemoConfig,
) )
} }

View file

@ -483,4 +483,12 @@ internal object WalletsDomainModule {
dispatchers = dispatcherProvider, dispatchers = dispatcherProvider,
) )
} }
@Provides
@Singleton
fun provideHasSecuredWalletsUseCase(
userWalletsListRepository: UserWalletsListRepository,
): HasSecuredWalletsUseCase {
return HasSecuredWalletsUseCase(userWalletsListRepository = userWalletsListRepository)
}
} }

View file

@ -2,11 +2,11 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.Wallet
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -30,11 +30,12 @@ class TapWalletManager(
// ensuring that only one job is active at any given time. // ensuring that only one job is active at any given time.
loadUserWalletDataJob = CoroutineScope(dispatchers.io) loadUserWalletDataJob = CoroutineScope(dispatchers.io)
.launch { loadUserWalletData(userWallet) } .launch { loadUserWalletData(userWallet) }
.also { it.join() } .apply { join() }
} }
private suspend fun loadUserWalletData(userWallet: UserWallet) { private suspend fun loadUserWalletData(userWallet: UserWallet) {
Analytics.setContext(userWallet) val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.setContext(userWallet)
if (userWallet is UserWallet.Cold) { if (userWallet is UserWallet.Cold) {
val scanResponse = userWallet.scanResponse val scanResponse = userWallet.scanResponse

View file

@ -12,6 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R import com.tangem.core.ui.R
@ -47,6 +48,7 @@ import javax.inject.Singleton
internal class LegacyScanProcessor @Inject constructor( internal class LegacyScanProcessor @Inject constructor(
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val trackingContextProxy: TrackingContextProxy,
) { ) {
suspend fun scan( suspend fun scan(
@ -118,16 +120,14 @@ internal class LegacyScanProcessor @Inject constructor(
} }
} }
private fun sendAnalytics(analyticsEvent: AnalyticsEvent?, scanResponse: ScanResponse) { private fun sendAnalytics(analyticsEvent: AnalyticsEvent, scanResponse: ScanResponse) {
analyticsEvent?.let { event -> // this workaround needed to send CardWasScannedEvent without adding a context
// this workaround needed to send CardWasScannedEvent without adding a context val interceptor = CardContextInterceptor(scanResponse)
val interceptor = CardContextInterceptor(scanResponse) val params = analyticsEvent.params.toMutableMap()
val params = event.params.toMutableMap() interceptor.intercept(params)
interceptor.intercept(params) analyticsEvent.params = params.toMap()
event.params = params.toMap()
Analytics.send(event) Analytics.send(analyticsEvent)
}
} }
// TODO: [REDACTED_JIRA] // TODO: [REDACTED_JIRA]
@ -211,7 +211,7 @@ internal class LegacyScanProcessor @Inject constructor(
}, },
) { ) {
if (OnboardingHelper.isOnboardingCase(scanResponse)) { if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse) trackingContextProxy.addContext(scanResponse)
onWalletNotCreated() onWalletNotCreated()
navigateTo( navigateTo(
AppRoute.Onboarding( AppRoute.Onboarding(
@ -220,7 +220,7 @@ internal class LegacyScanProcessor @Inject constructor(
), ),
) { onProgressStateChange(it) } ) { onProgressStateChange(it) }
} else { } else {
Analytics.setContext(scanResponse) trackingContextProxy.setContext(scanResponse)
val wasTwinsOnboardingShown = val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()

View file

@ -3,15 +3,12 @@ package com.tangem.tap.domain.scanCard.chains
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
@ -34,9 +31,11 @@ class CheckForOnboardingChain(
) : ResultChain<ScanCardException, ScanResponse>() { ) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
return when { return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> { OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult) trackingContextProxy.addContext(previousChainResult)
ScanChainException.OnboardingNeeded( ScanChainException.OnboardingNeeded(
AppRoute.Onboarding( AppRoute.Onboarding(
scanResponse = previousChainResult, scanResponse = previousChainResult,
@ -45,7 +44,7 @@ class CheckForOnboardingChain(
).left() ).left()
} }
else -> { else -> {
Analytics.setContext(previousChainResult) trackingContextProxy.setContext(previousChainResult)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync() .invokeSync()

View file

@ -18,13 +18,13 @@ import com.tangem.core.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey 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.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.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.* import com.tangem.domain.visa.model.*
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
@ -44,6 +44,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask 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.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
@ -62,6 +63,7 @@ internal class DefaultTangemSdkManager(
private val resources: Resources, private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler, private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : TangemSdkManager { ) : TangemSdkManager {
@ -76,13 +78,12 @@ internal class DefaultTangemSdkManager(
secureStorage = tangemSdk.secureStorage, secureStorage = tangemSdk.secureStorage,
) )
} }
override val needEnrollBiometrics: Boolean
get() = tangemSdk.authenticationManager.needEnrollBiometrics
override val canUseBiometry: Boolean override val canUseBiometry: Boolean
get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
override val needEnrollBiometrics: Boolean
get() = tangemSdk.authenticationManager.needEnrollBiometrics
override val keystoreManager: KeystoreManager override val keystoreManager: KeystoreManager
get() = tangemSdk.keystoreManager get() = tangemSdk.keystoreManager
@ -511,6 +512,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 // endregion
companion object { companion object {

View file

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

View file

@ -1,7 +1,11 @@
package com.tangem.tap.domain.sdk.mocks.content package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse 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.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey 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), 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), 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( 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), 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), 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( 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), 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), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 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( ByteArrayKey(
@ -244,14 +270,49 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap( ExtendedPublicKeysMap(
mapOf( mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano 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), 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, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended 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), 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, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -296,9 +357,16 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // xrp DerivationPath("m/44'/1729'/0'/0'") to ExtendedPublicKey( // Tezos
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, -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(-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), 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, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
@ -312,19 +380,54 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap( ExtendedPublicKeysMap(
mapOf( mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano 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), 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, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended 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), 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, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 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

@ -14,10 +14,11 @@ import com.tangem.common.map
import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingCommand import com.tangem.operations.backup.StartPrimaryCardLinkingCommand
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
@ -60,7 +61,13 @@ class CreateProductWalletTask(
val cardDto = CardDTO(card) val cardDto = CardDTO(card)
val commandProcessor = when { val commandProcessor = when {
cardTypesResolver.isTangemNote() -> CreateWalletTangemNote(cardTypesResolver) /**
* @workaround isDemoNoteAsMultiwallet
* There were produced 20k Note demo cards that should work like multiwallet (except Onboarding)
* for that reasons we've just added some specific checks for their BatchId
*/
DemoConfig.isDemoNoteAsMultiwallet(card.cardId) || cardTypesResolver.isTangemNote() ->
CreateWalletTangemNote(cardTypesResolver)
cardTypesResolver.isTangemTwins() -> cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
@ -374,7 +381,7 @@ private class CreateWalletTangemWallet(
private fun getBlockchains(cardId: String, card: CardDTO): List<Blockchain> { private fun getBlockchains(cardId: String, card: CardDTO): List<Blockchain> {
return when { return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains.toList() DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.getDemoBlockchains(cardId).toList()
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet) card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum) else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
} }

View file

@ -38,7 +38,7 @@ internal class DerivationsFinder(
getBlockchains(userWalletId) getBlockchains(userWalletId)
}.ifEmpty { }.ifEmpty {
if (DemoHelper.isDemoCardId(card.cardId)) { if (DemoHelper.isDemoCardId(card.cardId)) {
getDemoBlockchains(derivationStyle) getDemoBlockchains(derivationStyle, card.cardId)
} else { } else {
getDefaultBlockchains(derivationStyle) getDefaultBlockchains(derivationStyle)
} }
@ -77,8 +77,8 @@ internal class DerivationsFinder(
} }
// TODO: Move to user wallet config // TODO: Move to user wallet config
private fun getDemoBlockchains(derivationStyle: DerivationStyle?): MutableSet<BlockchainToDerive> { private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet<BlockchainToDerive> {
return DemoHelper.config.demoBlockchains.mapToBlockchainsWithDerivations(derivationStyle) return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
} }
// TODO: Move to user wallet config // TODO: Move to user wallet config

View file

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

@ -177,7 +177,7 @@ class VisaCardActivationTask @AssistedInject constructor(
.getOrElse { raise(it.tangemError) } .getOrElse { raise(it.tangemError) }
if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) {
return raise(VisaActivationError.WrongRemoteState.tangemError) raise(VisaActivationError.WrongRemoteState.tangemError)
} }
visaActivationRepository.getCardWalletAcceptanceData( visaActivationRepository.getCardWalletAcceptanceData(

View file

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

View file

@ -36,11 +36,6 @@ internal class BiometricUserWalletsListManager(
.mapLatest { it.userWallets } .mapLatest { it.userWallets }
.distinctUntilChanged() .distinctUntilChanged()
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override val userWalletsSync: List<UserWallet> override val userWalletsSync: List<UserWallet>
get() = state.value.userWallets get() = state.value.userWallets
@ -71,6 +66,11 @@ internal class BiometricUserWalletsListManager(
override val walletsCount: Int override val walletsCount: Int
get() = state.value.userWallets.size get() = state.value.userWallets.size
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> { override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
return unlockAndSetSelectedUserWallet(type) return unlockAndSetSelectedUserWallet(type)
.mapFailure { error -> .mapFailure { error ->

View file

@ -20,11 +20,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
.mapLatest { listOfNotNull(it.userWallet) } .mapLatest { listOfNotNull(it.userWallet) }
.distinctUntilChanged() .distinctUntilChanged()
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet> override val selectedUserWallet: Flow<UserWallet>
get() = state get() = state
.mapLatest { it.userWallet } .mapLatest { it.userWallet }
@ -46,6 +41,11 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
override val walletsCount: Int override val walletsCount: Int
get() = if (hasUserWallets) 1 else 0 get() = if (hasUserWallets) 1 else 0
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching { override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet state.value.userWallet
?.takeIf { it.walletId == userWalletId } ?.takeIf { it.walletId == userWalletId }

View file

@ -27,6 +27,7 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.extensions.indexOfFirstOrNull import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -149,7 +150,7 @@ internal class DefaultUserWalletsListRepository(
val encryptionKey = userWallet.encryptionKey val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked) ?: raise(SetLockError.UserWalletLocked)
runCatching { runSuspendCatching {
userWalletEncryptionKeysRepository.save( userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey( encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId, walletId = userWalletId,
@ -232,7 +233,7 @@ internal class DefaultUserWalletsListRepository(
val encryptionKey = requestPasswordRecursive( val encryptionKey = requestPasswordRecursive(
hotWalletId = userWallet.hotWalletId, hotWalletId = userWallet.hotWalletId,
block = { password -> block = { password ->
runCatching { runSuspendCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
}.onFailure { }.onFailure {
raise(UnlockWalletError.UnableToUnlock) raise(UnlockWalletError.UnableToUnlock)
@ -288,8 +289,7 @@ internal class DefaultUserWalletsListRepository(
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either { override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWallets = userWalletsSync() val userWallets = userWalletsSync()
val userWalletIds = userWallets.map { it.walletId }.toSet() val biometricKeys = runSuspendCatching {
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric() userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse { }.getOrElse {
// TODO handle error properly [REDACTED_TASK_KEY] // TODO handle error properly [REDACTED_TASK_KEY]
@ -309,8 +309,9 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(it) removePasswordAttempts(it)
} }
// if we cant unlock all wallets // if we cant unlock any of the locked wallets, return error
if (userWalletIds.all { it in unlockedWalletsIds }.not()) { // (isLocked remains `true` here because we haven't updated the wallets yet)
if (unlockedWallets.any { it.isLocked }.not()) {
raise(UnlockWalletError.UnableToUnlock) raise(UnlockWalletError.UnableToUnlock)
} }
@ -345,6 +346,12 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.clear() userWalletEncryptionKeysRepository.clear()
} }
override suspend fun hasSecuredWallets(): Boolean {
val userWallets = userWalletsSync()
val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet()
return userWallets.any { it.walletId !in unsecuredWalletIds }
}
private suspend fun requestPasswordRecursive( private suspend fun requestPasswordRecursive(
hotWalletId: HotWalletId, hotWalletId: HotWalletId,
block: suspend (CharArray) -> UserWalletEncryptionKey?, block: suspend (CharArray) -> UserWalletEncryptionKey?,

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
object DemoHelper { object DemoHelper {
val config = DemoConfig() val config = DemoConfig
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)

View file

@ -34,6 +34,7 @@ import org.rekotlin.Action
import org.rekotlin.Middleware import org.rekotlin.Middleware
import timber.log.Timber import timber.log.Timber
@Suppress("MemberNameEqualsClassName")
class DetailsMiddleware { class DetailsMiddleware {
private val appSettingsMiddleware = AppSettingsMiddleware() private val appSettingsMiddleware = AppSettingsMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider -> val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
@ -66,14 +67,8 @@ class DetailsMiddleware {
when (action.setting) { when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode( AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
state = state, AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
enable = action.enable,
)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(
state = state,
enable = action.enable,
)
} }
} }
is DetailsAction.AppSettings.CheckBiometricsStatus -> { is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -100,7 +95,7 @@ class DetailsMiddleware {
} }
} }
private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) { private fun toggleBiometricsAuthentication(enable: Boolean) {
scope.launch { scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository) val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
@ -110,16 +105,12 @@ class DetailsMiddleware {
return@launch return@launch
} }
toggleRequireAccessCode(
state = state,
enable = true,
)
if (enable) { if (enable) {
setBiometricLockForAllWallets() setBiometricLockForAllWallets()
} else { } else {
// Remove all biometric-related data // Remove all biometric-related data
removeAllBiometricData() removeAllBiometricData()
walletsRepository.setRequireAccessCode(value = true)
} }
walletsRepository.setUseBiometricAuthentication(value = enable) walletsRepository.setUseBiometricAuthentication(value = enable)
@ -127,7 +118,7 @@ class DetailsMiddleware {
} }
} }
private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) { private fun toggleRequireAccessCode(enable: Boolean) {
scope.launch { scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository) val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
@ -138,11 +129,8 @@ class DetailsMiddleware {
} }
if (enable) { if (enable) {
// Remove all biometric sign data // Remove all saved access codes
removeAllBiometricSingData() removeAllBiometricSingData()
toggleSaveAccessCodes(state, enable = false)
} else {
toggleSaveAccessCodes(state, enable = true)
} }
walletsRepository.setRequireAccessCode(value = enable) walletsRepository.setRequireAccessCode(value = enable)

View file

@ -111,6 +111,9 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isHidingEnabled = action.state.isHidingEnabled, isHidingEnabled = action.state.isHidingEnabled,
selectedAppCurrency = action.state.selectedAppCurrency, selectedAppCurrency = action.state.selectedAppCurrency,
selectedThemeMode = action.state.selectedThemeMode, selectedThemeMode = action.state.selectedThemeMode,
useBiometricAuthentication = action.state.useBiometricAuthentication,
requireAccessCode = action.state.requireAccessCode,
hasSecuredWallets = action.state.hasSecuredWallets,
), ),
) )
is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.EnrollBiometrics,

View file

@ -22,6 +22,7 @@ data class AppSettingsState(
val requireAccessCode: Boolean = false, val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false, val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false, val needEnrollBiometrics: Boolean = false,
val hasSecuredWallets: Boolean = false,
val isHidingEnabled: Boolean = false, val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false, val isInProgress: Boolean = false,
val selectedAppCurrency: AppCurrency = AppCurrency.Default, val selectedAppCurrency: AppCurrency = AppCurrency.Default,

View file

@ -10,6 +10,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
@ -48,6 +49,7 @@ internal class AppSettingsModel @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository, private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository, private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val balanceHidingRepository: BalanceHidingRepository, private val balanceHidingRepository: BalanceHidingRepository,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val appThemeModeRepository: AppThemeModeRepository, private val appThemeModeRepository: AppThemeModeRepository,
@ -114,7 +116,8 @@ internal class AppSettingsModel @Inject constructor(
) )
if (hotWalletFeatureToggles.isHotWalletEnabled) { if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress val canUseBiometrics =
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
add( add(
itemsFactory.createUseBiometricsSwitch( itemsFactory.createUseBiometricsSwitch(
@ -126,7 +129,7 @@ internal class AppSettingsModel @Inject constructor(
add( add(
itemsFactory.createRequireAccessCodeSwitch( itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode, isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
isEnabled = canUseBiometrics && state.useBiometricAuthentication, isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled, onCheckedChange = ::onRequireAccessCodeToggled,
), ),
@ -206,14 +209,12 @@ internal class AppSettingsModel @Inject constructor(
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param)) // analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) { if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true) onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
} else { } else {
updateContentState { updateContentState {
copy( copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = { onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false) onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog() dismissDialog()
}, },
onDismiss = ::dismissDialog, onDismiss = ::dismissDialog,
@ -323,6 +324,7 @@ internal class AppSettingsModel @Inject constructor(
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
) )
store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state)) store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state))

View file

@ -4,9 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R import com.tangem.core.ui.R
@ -16,14 +16,11 @@ import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager 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.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.SendPushTokenUseCase
@ -41,9 +38,7 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -83,9 +78,10 @@ internal class MainViewModel @Inject constructor(
private val apiConfigsManager: ApiConfigsManager, private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater, private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder, private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig, private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
private val trackingContextProxy: TrackingContextProxy,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() { ) : ViewModel() {
@ -181,7 +177,7 @@ internal class MainViewModel @Inject constructor(
.mapLeft { emptyFlow<UserWallet>() } .mapLeft { emptyFlow<UserWallet>() }
.onRight { wallet -> .onRight { wallet ->
wallet.distinctUntilChanged() wallet.distinctUntilChanged()
.onEach { Analytics.setContext(it) } .onEach { trackingContextProxy.setContext(it) }
.flowOn(dispatchers.io) .flowOn(dispatchers.io)
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
@ -195,22 +191,12 @@ internal class MainViewModel @Inject constructor(
private fun initializeOffRamp() { private fun initializeOffRamp() {
viewModelScope.launch { viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService appStateHolder.sellService = sellService
sellService.update() 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() { private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope) listenToFlipsUseCase().launchIn(viewModelScope)
} }

View file

@ -14,6 +14,7 @@ import com.tangem.tap.store
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.Middleware import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
class BackupMiddleware { class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state -> val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next -> { next ->

View file

@ -77,9 +77,13 @@ internal class WelcomeModel @Inject constructor(
warning = warning, warning = warning,
error = state.error error = state.error
?.takeIf { !it.silent && warning == null } ?.takeIf { !it.silent && warning == null }
?.let { e -> ?.let { error ->
e.messageResId?.let { TextReference.Res(it) } val messageResId = error.messageResId
?: TextReference.Str(e.customMessage) if (messageResId != null) {
TextReference.Res(messageResId)
} else {
TextReference.Str(error.customMessage)
}
}, },
) )
} }

View file

@ -100,7 +100,10 @@ internal class WelcomeMiddleware {
val currency = ParamCardCurrencyConverter().convert( val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver, value = scanResponse.cardTypesResolver,
) )
Analytics.addContext(scanResponse)
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.addContext(scanResponse)
if (currency != null) { if (currency != null) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)

View file

@ -0,0 +1,20 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
private val environmentConfigStorage: EnvironmentConfigStorage,
) : P2PEthPoolAuthProvider {
override fun getApiKey(): String {
// val keys = environmentConfigStorage.getConfigSync().p2pApiKey
// ?: error("No P2P api keys provided")
//
// return keys.mainnet
environmentConfigStorage
return "TODO restore p2p config call after release 5.30"
}
}

View file

@ -6,10 +6,12 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider
import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultP2PEthPoolAuthProvider
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
import com.tangem.utils.version.AppVersionProvider import com.tangem.utils.version.AppVersionProvider
import dagger.Module import dagger.Module
@ -50,6 +52,12 @@ internal class AuthModule {
return DefaultStakeKitAuthProvider(environmentConfigStorage) return DefaultStakeKitAuthProvider(environmentConfigStorage)
} }
@Provides
@Singleton
fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider {
return DefaultP2PEthPoolAuthProvider(environmentConfigStorage)
}
@Provides @Provides
@Singleton @Singleton
fun provideAppVersionProvider(): AppVersionProvider { fun provideAppVersionProvider(): AppVersionProvider {

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