Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-26 14:12:43 +03:00
commit c306b16a7b
1308 changed files with 41564 additions and 8590 deletions

View file

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

View file

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

View file

@ -4,8 +4,32 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val ETHEREUM_ADDRESS = "0x9FFd974772bDA94D288240C1B22F367Ce75CcD7f"
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val CARDANO_ADDRESS =
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
const val POLKADOT_RECIPIENT_ADDRESS = "143TfgFYAFfM86LRzt4UcFNU3KosxCndBCVz2U5HCxpLidKZ"
const val XRP_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_LONG = 30_000L
@ -14,4 +38,7 @@ object TestConstants {
const val ALLURE_LABEL_NAME = "Owner"
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val QUOTES_API_SCENARIO = "quotes_api"
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,41 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onReceiveAssetsBottomSheet
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
import com.tangem.screens.onTokenReceiveWarningBottomSheet
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.goToQrCodeBottomSheet() {
step("Assert 'Token receive warning' bottom sheet is displayed") {
onTokenReceiveWarningBottomSheet { bottomSheet.assertIsDisplayed() }
}
step("Click on 'Got it' button") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
}
fun BaseTestCase.checkQrCodeBottomSheetScenario() {
step("Assert bottom sheet with QR code title is displayed") {
onTokenReceiveQrCodeBottomSheet { title.assertIsDisplayed() }
}
step("Assert QR code is displayed") {
onTokenReceiveQrCodeBottomSheet { qrCode.assertIsDisplayed() }
}
step("Assert address title is displayed") {
onTokenReceiveQrCodeBottomSheet { addressTitle.assertIsDisplayed() }
}
step("Assert address is displayed") {
onTokenReceiveQrCodeBottomSheet { address.assertIsDisplayed() }
}
step("Assert 'Copy' button is displayed") {
onTokenReceiveQrCodeBottomSheet { copyButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,141 @@
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' with description '$description' is displayed") {
onSendAddressScreen {
recentAddressItem(recipientAddress = address, description = description).assertIsDisplayed()
}
}
}

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
}
val tokenAmountField: KNode = child {
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT))
useUnmergedTree = true
}
val recommendedTitle: KNode = child {
hasText(getResourceString(OnrampImplR.string.onramp_recommended_title))
}
val bestRateIcon: KNode = child {
hasTestTag(OnrampOffersBlockTestTags.BEST_RATE_ICON)
useUnmergedTree = true
}
val bestRateTitle: KNode = child {
hasTestTag(OnrampOffersBlockTestTags.BEST_RATE_TITLE)
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)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.common.ui.R as CommonUIR
class OperationIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<OperationIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.token_button_unavailability_generic_description))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onOperationIsUnavailableDialog(function: OperationIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

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

View file

@ -3,24 +3,164 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.FooterTestTags
import com.tangem.core.ui.test.SendAddressScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.features.send.v2.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.ui.R as CoreUiR
class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val container: KNode = child {
hasTestTag(SendAddressScreenTestTags.CONTAINER)
useUnmergedTree = true
}
val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(CoreUiR.string.common_address))
useUnmergedTree = true
}
val addressTextFieldTitle: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE)
useUnmergedTree = true
}
val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true
}
val addressTextFieldHint: KNode = child {
hasParent(withTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD))
useUnmergedTree = true
}
fun recipientNetworkCaution(network: String): KNode = child {
val expectedText = getResourceString(R.string.send_recipient_address_footer, network)
.replace("**", "")
hasText(expectedText, substring = true)
hasTestTag(FooterTestTags.FOOTER_TEXT)
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,
description: String? = null
): 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 (description != null) {
hasAnyDescendant(withText(description, substring = 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 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 {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_next)))
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_next))
hasText(getResourceString(R.string.common_continue))
useUnmergedTree = true
}

View file

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

View file

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

View file

@ -0,0 +1,35 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.common.ui.R as CommonUIR
class SwapIsNotSupportedDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapIsNotSupportedDialogPageObject>(semanticsProvider = semanticsProvider) {
fun text(currencyName: String): KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(
getResourceString(
CommonUIR.string.token_button_unavailability_reason_not_exchangeable,
currencyName
)
)
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onSwapIsNotSupportedDialog(function: SwapIsNotSupportedDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

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

View file

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

View file

@ -70,7 +70,7 @@ class BuyTokenTest : BaseTestCase() {
val otherCurrenciesTitle = "Other currencies"
val australianDollar = "AUD"
val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
@ -98,10 +98,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen {
tokenAmountField.assertTextContains(tokenAmount)
}
step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
@ -129,10 +127,8 @@ class BuyTokenTest : BaseTestCase() {
fiatAmountTextField.assertTextContains(australianDollar + fiatAmount)
}
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen {
tokenAmountField.assertTextContains(tokenAmount)
}
step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
}
}
@ -145,7 +141,7 @@ class BuyTokenTest : BaseTestCase() {
val tokenTitle = "Polygon"
val euro = "EUR"
val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
@ -182,18 +178,12 @@ class BuyTokenTest : BaseTestCase() {
step("Assert fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) }
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Assert 'Recommended' title is displayed") {
onBuyTokenDetailsScreen { recommendedTitle.clickWithAssertion() }
}
step("Assert 'Best rate' icon is displayed") {
onBuyTokenDetailsScreen { bestRateIcon.assertIsDisplayed() }
}
step("Assert 'Best rate' title is displayed") {
onBuyTokenDetailsScreen { bestRateTitle.assertIsDisplayed() }
}
@ -316,7 +306,7 @@ class BuyTokenTest : BaseTestCase() {
val paymentMethod = "Invoice Revolut Pay"
val fiatAmount = "1"
val providerNameMercuryo = "Mercuryo"
val tokenAmount = "~488.24938338 POL"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
@ -344,11 +334,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen { providerName.assertIsDisplayed() }
@ -365,9 +352,6 @@ class BuyTokenTest : BaseTestCase() {
step("Assert 'Provider' bottom sheet subtitle is displayed") {
onSelectProviderBottomSheet { subtitle.assertIsDisplayed() }
}
step("Assert 'Best rate' icon is displayed") {
onSelectProviderBottomSheet { bestRateIcon.assertIsDisplayed() }
}
step("Assert 'Best rate' title is displayed") {
onSelectProviderBottomSheet { bestRateTitle.assertIsDisplayed() }
}
@ -406,7 +390,7 @@ class BuyTokenTest : BaseTestCase() {
val invoiceRevolutPay = "Invoice Revolut Pay"
val sepa = "Sepa"
val fiatAmount = "1"
val tokenAmount = "~488.24938338 POL"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
@ -434,11 +418,8 @@ class BuyTokenTest : BaseTestCase() {
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) }
}
step("Click on 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.clickWithAssertion() }
step("Assert offer token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { offerTokenAmount.assertTextContains(tokenAmount) }
}
step("Assert 'Provider block' is displayed") {
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.store
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@ -37,7 +36,11 @@ class FeedbackTest : BaseTestCase() {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click 'More' button on TopBar") {
waitForIdle()
onTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Contact support' button") {
@ -73,7 +76,7 @@ class FeedbackTest : BaseTestCase() {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
@ -130,7 +133,7 @@ class FeedbackTest : BaseTestCase() {
MockProvider.resetEmulateError()
}
).run {
Allure.step("Click on 'Accept' button") {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Set scanning error") {

View file

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

View file

@ -1,5 +1,6 @@
package com.tangem.tests
import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
@ -8,7 +9,7 @@ import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.*
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScanQrScreen
import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectScreen
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.testing.HiltAndroidTest
@ -196,8 +197,14 @@ class WalletConnectTest : BaseTestCase() {
val dAppName = "React App"
val context = device.context
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") {
setClipboardText(context, deepLinkUri)
}
@ -214,7 +221,7 @@ class WalletConnectTest : BaseTestCase() {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("CLick 'Paste from clipboard' button") {
onWalletConnectScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()

View file

@ -2,14 +2,19 @@ package com.tangem.tests.actionButtons
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.common.extensions.*
import com.tangem.common.utils.*
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -18,6 +23,7 @@ import org.junit.Test
@HiltAndroidTest
class MainScreenActionButtonsTest : BaseTestCase() {
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("79")
@DisplayName("Action buttons (long tap): validate UI")
@Test
@ -121,7 +127,6 @@ class MainScreenActionButtonsTest : BaseTestCase() {
@Test
fun clickOnBuyButtonTest() {
val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
setupHooks().run {
step("Open 'Main Screen'") {
@ -160,12 +165,6 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Assert fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
}
step("Assert token amount field is displayed") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenSymbol, substring = true) }
}
step("Assert 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.assertIsDisplayed() }
}
}
}
@ -281,41 +280,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Click on 'Receive' button") {
onTokenActionsBottomSheet { receiveButton.performClick() }
}
step("Assert 'Token receive warning' bottom sheet is displayed") {
waitForIdle()
step("Go to QR code bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onTokenReceiveWarningBottomSheet {
bottomSheet.assertIsDisplayed()
}
goToQrCodeBottomSheet()
}
}
step("Click on 'Got it' button") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
step("Assert bottom sheet with QR code title is displayed") {
onTokenReceiveQrCodeBottomSheet { title.assertIsDisplayed() }
}
step("Assert QR code is displayed") {
onTokenReceiveQrCodeBottomSheet { qrCode.assertIsDisplayed() }
}
step("Assert address title is displayed") {
onTokenReceiveQrCodeBottomSheet { addressTitle.assertIsDisplayed() }
}
step("Assert address is displayed") {
onTokenReceiveQrCodeBottomSheet { address.assertIsDisplayed() }
}
step("Assert 'Copy' button is displayed") {
onTokenReceiveQrCodeBottomSheet { copyButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
step("Check QR code bottom sheet") {
checkQrCodeBottomSheetScenario()
}
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("85")
@DisplayName("Action buttons (long tap): check 'Sell' button")
@Test
@ -342,10 +318,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
}
}
}
step("Assert 'Receive' button is displayed") {
step("Assert 'Sell' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
}
step("Click on 'Receive' button") {
step("Click on 'Sell' button") {
onTokenActionsBottomSheet { sellButton.performClick() }
}
step("Assert Chrome Browser is opened") {
@ -391,4 +367,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,142 @@
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 'My wallets' item is not displayed") {
onSendAddressScreen {
recentAddressItem(
recipientAddress = ETHEREUM_ADDRESS,
isMyWallet = true
).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,355 @@
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 recentTransactionAmount1 = "DOGE 0.0192"
private val recentTransactionAmount2 = "DOGE 0.10"
private val recentTransactionAmount3 = "DOGE 0.005"
@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("Click on top bar 'Close' button") {
waitForIdle()
onSendConfirmScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Send Address' screen is displayed") {
waitForIdle()
onSendAddressScreen { container.assertIsDisplayed() }
}
step("Assert address text field is displayed") {
onSendAddressScreen { addressTextField.assertIsDisplayed() }
}
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 = "Dogecoin"
val sendAmount = "1"
val txHistoryScenarioState = "Empty"
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 not displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
step("Assert recent address item is not displayed") {
onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).assertIsNotDisplayed() }
}
}
}
@AllureId("4598")
@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"
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 = recentTransactionAmount1)
}
step("Check recent address item №2") {
checkRecentAddressItem(address = shortenedRecipientAddress, description = recentTransactionAmount2)
}
step("Check recent address item №3") {
checkRecentAddressItem(address = recipientAddressBase + "k", description = recentTransactionAmount2)
}
step("Check recent address item №4") {
checkRecentAddressItem(address = recipientAddressBase + "c", description = recentTransactionAmount2)
}
step("Check recent address item №5") {
checkRecentAddressItem(address = recipientAddressBase + "d", description = recentTransactionAmount2)
}
step("Check recent address item №6") {
checkRecentAddressItem(address = recipientAddressBase + "e", description = recentTransactionAmount2)
}
step("Swipe up") {
waitForIdle()
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
}
step("Check recent address item №7") {
checkRecentAddressItem(address = recipientAddressBase + "f", description = recentTransactionAmount2)
}
step("Check recent address item №8") {
checkRecentAddressItem(address = recipientAddressBase + "g", description = recentTransactionAmount2)
}
step("Check recent address item №9") {
checkRecentAddressItem(address = recipientAddressBase + "o", description = recentTransactionAmount2)
}
step("Check recent address item №10") {
checkRecentAddressItem(address = recipientAddressBase + "p", description = recentTransactionAmount2)
}
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"
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 = recentTransactionAmount1)
}
step("Check recent address item №2") {
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = recentTransactionAmount3)
}
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,629 @@
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("4009")
@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() }
}
}
}
@AllureId("4567")
@DisplayName("Send (address screen): check address field")
@Test
fun checkSendAddressScreenTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val hint = getResourceString(R.string.send_enter_address_field_ens)
val recipient = getResourceString(R.string.send_recipient)
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send Address' screen") {
openSendAddressScreen(tokenName, sendAmount)
}
step("Assert 'Address' screen title is displayed") {
onSendAddressScreen { topAppBarTitle.assertIsDisplayed() }
}
step("Assert address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(recipient) }
}
step("Assert address text field is displayed") {
onSendAddressScreen { addressTextField.assertIsDisplayed() }
}
step("Assert address text field hint contains text '$hint'") {
onSendAddressScreen { addressTextFieldHint.assertTextContains(hint) }
}
step("Assert 'QR' button is displayed") {
onSendAddressScreen { qrButton.assertIsDisplayed() }
}
step("Assert 'Paste' button is displayed") {
onSendAddressScreen { addressPasteButton.assertIsDisplayed() }
}
step("Assert recipient network caution is displayed for network '$tokenName'") {
onSendAddressScreen { recipientNetworkCaution(tokenName).assertIsDisplayed() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
}

View file

@ -0,0 +1,309 @@
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.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.*
import com.tangem.common.utils.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 {
flakySafely(WAIT_UNTIL_TIMEOUT) {
refreshButton.clickWithAssertion()
refreshButton.assertDoesNotExist()
}
}
}
step("Check network fee block") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
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

@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
@ -310,4 +313,8 @@
</intent-filter>
</service>
</application>
<queries>
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
<package android:name="com.google.android.apps.walletnfcrel" />
</queries>
</manifest>

View file

@ -5,7 +5,9 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
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.utils.TrackingContextProxy
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.GlobalUiMessageSender
@ -151,4 +153,8 @@ interface ApplicationEntryPoint {
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase
fun getTrackingContextProxy(): TrackingContextProxy
fun getABTestsManager(): ABTestsManager
}

View file

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

View file

@ -16,6 +16,7 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -240,6 +241,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
private val trackingContextProxy
get() = entryPoint.getTrackingContextProxy()
private val abTestsManager: ABTestsManager
get() = entryPoint.getABTestsManager()
// endregion
private val appScope = MainScope()
@ -310,6 +317,8 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
abTestsManager.init()
appScope.launch {
launch(Dispatchers.IO) {
loadNativeLibraries()
@ -385,6 +394,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
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,
exceptionHost: String,
error: String,
blockchain: String,
) : AnalyticsEvent(
category = "BlockchainSdk",
event = "Exception",
@ -13,5 +14,6 @@ class BlockchainApiExceptionEvent(
AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost,
AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost,
AnalyticsParam.ERROR_DESCRIPTION to error,
AnalyticsParam.BLOCKCHAIN to blockchain,
),
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,8 +54,8 @@ class PushNotificationDelegate(private val context: Context) {
.setContentIntent(pendingIntent)
.setVibrate(vibratePattern)
.apply {
imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
if (imageUrl != null) {
val bitmap = getBitmapImageFromUrl(imageUrl)
setStyle(
NotificationCompat
.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) {
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 org.rekotlin.Action
@Suppress("CanBeNonNullable")
fun appReducer(action: Action, state: AppState?): AppState {
requireNotNull(state)
if (action is AppAction.RestoreState) return action.state

View file

@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
internal object LegacyMiddleware {
private val prepareDetailsScreenJobHolder = JobHolder()
@ -29,7 +30,14 @@ internal object LegacyMiddleware {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
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 ->
val initializedAppSettingsStateContent = initializeAppSettingsState(
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
@ -78,6 +86,7 @@ internal object LegacyMiddleware {
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
hasSecuredWallets = store.inject(DaggerGraphState::userWalletsListRepository).hasSecuredWallets(),
)
}
}

View file

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

View file

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

View file

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

View file

@ -4,9 +4,9 @@ import android.content.Context
import android.os.Build
import android.os.Vibrator
import android.os.VibratorManager
import com.tangem.tap.common.haptic.DefaultVibratorHapticManager
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.tap.common.haptic.DefaultVibratorHapticManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -22,10 +22,14 @@ class HapticModule {
@Singleton
fun provideHapticManager(@ApplicationContext context: Context): VibratorHapticManager {
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
} 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()) {

View file

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

View file

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

View file

@ -3,8 +3,14 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -32,17 +38,27 @@ internal object AccountDomainModule {
@Provides
@Singleton
fun provideUpdateCryptoPortfolioUseCase(
singleAccountListFetcher: SingleAccountListFetcher,
accountsCRUDRepository: AccountsCRUDRepository,
): UpdateCryptoPortfolioUseCase {
return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
return UpdateCryptoPortfolioUseCase(
singleAccountListFetcher = singleAccountListFetcher,
crudRepository = accountsCRUDRepository,
)
}
@Provides
@Singleton
fun provideArchiveCryptoPortfolioUseCase(
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsCRUDRepository: AccountsCRUDRepository,
referralRepository: ExternalReferralRepository,
): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
return ArchiveCryptoPortfolioUseCase(
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
crudRepository = accountsCRUDRepository,
referralRepository = referralRepository,
)
}
@Provides
@ -50,10 +66,12 @@ internal object AccountDomainModule {
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
)
}
@ -82,4 +100,16 @@ internal object AccountDomainModule {
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
@Singleton
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig)
@Provides
@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,8 @@
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.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository
@ -24,9 +27,13 @@ internal object NFTDomainModule {
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
@Provides
@ -60,10 +67,12 @@ internal object NFTDomainModule {
@Singleton
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
)
@Provides
@ -138,8 +147,15 @@ internal object NFTDomainModule {
fun provideClearNFTCacheUseCase(
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
accountsFeatureToggles: AccountsFeatureToggles,
singleAccountListSupplier: SingleAccountListSupplier,
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
return ObserveAndClearNFTCacheIfNeedUseCase(
nftRepository = nftRepository,
currenciesRepository = currenciesRepository,
accountsFeatureToggles = accountsFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,
)
}
@Provides

View file

@ -0,0 +1,41 @@
package com.tangem.tap.di.domain
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
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 NewsDomainModule {
@Provides
@Singleton
fun provideGetNewsCategoriesUseCase(repository: NewsRepository): GetNewsCategoriesUseCase {
return GetNewsCategoriesUseCase(repository)
}
@Provides
@Singleton
fun provideObserveNewsDetailsUseCase(repository: NewsRepository): ObserveNewsDetailsUseCase {
return ObserveNewsDetailsUseCase(repository)
}
@Provides
@Singleton
fun provideObserveTrendingNewsUseCase(repository: NewsRepository): ManageTrendingNewsUseCase {
return ManageTrendingNewsUseCase(repository)
}
@Provides
@Singleton
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
return GetNewsListBatchFlowUseCase(repository)
}
}

View file

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

View file

@ -1,10 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakeKitActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
@ -17,6 +19,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
internal object StakingDomainModule {
@Provides
@ -34,11 +37,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetStakingEntryInfoUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingEntryInfoUseCase {
return GetStakingEntryInfoUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -46,11 +49,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetYieldUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): GetYieldUseCase {
return GetYieldUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -58,13 +61,13 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideFetchActionsUseCase(
stakingRepository: StakingRepository,
stakingActionRepository: StakingActionRepository,
stakeKitRepository: StakeKitRepository,
stakeKitActionRepository: StakeKitActionRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchActionsUseCase {
return FetchActionsUseCase(
stakingRepository = stakingRepository,
stakingActionRepository = stakingActionRepository,
stakeKitRepository = stakeKitRepository,
stakeKitActionRepository = stakeKitActionRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -72,11 +75,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetActionsUseCase(
stakingActionRepository: StakingActionRepository,
stakeKitActionRepository: StakeKitActionRepository,
stakingErrorResolver: StakingErrorResolver,
): GetActionsUseCase {
return GetActionsUseCase(
stakingActionRepository = stakingActionRepository,
stakeKitActionRepository = stakeKitActionRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -84,11 +87,25 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetStakingTokensUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingTokensUseCase {
return FetchStakingTokensUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideFetchStakingOptionsUseCase(
stakeKitRepository: StakeKitRepository,
p2pRepository: P2PEthPoolRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingOptionsUseCase {
return FetchStakingOptionsUseCase(
stakeKitRepository = stakeKitRepository,
p2pRepository = p2pRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -108,11 +125,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetStakingTransactionsUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingTransactionsUseCase {
return GetStakingTransactionsUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -120,11 +137,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGasEstimateUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): EstimateGasUseCase {
return EstimateGasUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -132,11 +149,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSubmitHashUseCase(
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakeKitTransactionHashRepository: StakeKitTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SubmitHashUseCase {
return SubmitHashUseCase(
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakeKitTransactionHashRepository = stakeKitTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -144,11 +161,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSaveUnsubmittedHashUseCase(
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakeKitTransactionHashRepository: StakeKitTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SaveUnsubmittedHashUseCase {
return SaveUnsubmittedHashUseCase(
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakeKitTransactionHashRepository = stakeKitTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -166,11 +183,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSendUnsubmittedHashesUseCase(
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakeKitTransactionHashRepository: StakeKitTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SendUnsubmittedHashesUseCase {
return SendUnsubmittedHashesUseCase(
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakeKitTransactionHashRepository = stakeKitTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -178,11 +195,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetConstructedStakingTransactionUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingErrorResolver: StakingErrorResolver,
): GetConstructedStakingTransactionUseCase {
return GetConstructedStakingTransactionUseCase(
stakingRepository = stakingRepository,
stakeKitRepository = stakeKitRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -222,9 +239,9 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideStakingApyFlowUseCase(
stakingRepository: StakingRepository,
stakeKitRepository: StakeKitRepository,
stakingFeatureToggles: StakingFeatureToggles,
): StakingApyFlowUseCase {
return StakingApyFlowUseCase(stakingRepository, stakingFeatureToggles)
return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles)
}
}

View file

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

View file

@ -31,7 +31,7 @@ internal object TransactionDomainModule {
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
return GetFeeUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(),
demoConfig = DemoConfig,
)
}
@ -52,7 +52,7 @@ internal object TransactionDomainModule {
dispatchers: CoroutineDispatcherProvider,
): SendTransactionUseCase {
return SendTransactionUseCase(
demoConfig = DemoConfig(),
demoConfig = DemoConfig,
cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
@ -131,7 +131,7 @@ internal object TransactionDomainModule {
fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase {
return EstimateFeeUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(),
demoConfig = DemoConfig,
)
}

View file

@ -166,10 +166,34 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@Singleton
fun providesUnlockWalletUseCase(
nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
userWalletsListRepository: UserWalletsListRepository,
): UnlockWalletUseCase {
return UnlockWalletUseCase(
nonBiometricUnlockWalletUseCase = nonBiometricUnlockWalletUseCase,
userWalletsListRepository = userWalletsListRepository,
)
}
@Provides
@Singleton
fun providesNonBiometricUnlockWalletUseCase(
userWalletsListRepository: UserWalletsListRepository,
walletsRepository: WalletsRepository,
): NonBiometricUnlockWalletUseCase {
return NonBiometricUnlockWalletUseCase(
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesSelectWalletUseCase(
@ -483,4 +507,18 @@ internal object WalletsDomainModule {
dispatchers = dispatcherProvider,
)
}
@Provides
@Singleton
fun provideHasSecuredWalletsUseCase(
userWalletsListRepository: UserWalletsListRepository,
): HasSecuredWalletsUseCase {
return HasSecuredWalletsUseCase(userWalletsListRepository = userWalletsListRepository)
}
@Provides
@Singleton
fun provideSyncWalletWithRemoteUseCase(walletsRepository: WalletsRepository): SyncWalletWithRemoteUseCase {
return SyncWalletWithRemoteUseCase(walletsRepository = walletsRepository)
}
}

View file

@ -2,11 +2,11 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
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.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -30,11 +30,12 @@ class TapWalletManager(
// ensuring that only one job is active at any given time.
loadUserWalletDataJob = CoroutineScope(dispatchers.io)
.launch { loadUserWalletData(userWallet) }
.also { it.join() }
.apply { join() }
}
private suspend fun loadUserWalletData(userWallet: UserWallet) {
Analytics.setContext(userWallet)
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.setContext(userWallet)
if (userWallet is UserWallet.Cold) {
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.Basic
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.ui.UiMessageSender
import com.tangem.core.ui.R
@ -47,6 +48,7 @@ import javax.inject.Singleton
internal class LegacyScanProcessor @Inject constructor(
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val trackingContextProxy: TrackingContextProxy,
) {
suspend fun scan(
@ -118,16 +120,14 @@ internal class LegacyScanProcessor @Inject constructor(
}
}
private fun sendAnalytics(analyticsEvent: AnalyticsEvent?, scanResponse: ScanResponse) {
analyticsEvent?.let { event ->
// this workaround needed to send CardWasScannedEvent without adding a context
val interceptor = CardContextInterceptor(scanResponse)
val params = event.params.toMutableMap()
interceptor.intercept(params)
event.params = params.toMap()
private fun sendAnalytics(analyticsEvent: AnalyticsEvent, scanResponse: ScanResponse) {
// this workaround needed to send CardWasScannedEvent without adding a context
val interceptor = CardContextInterceptor(scanResponse)
val params = analyticsEvent.params.toMutableMap()
interceptor.intercept(params)
analyticsEvent.params = params.toMap()
Analytics.send(event)
}
Analytics.send(analyticsEvent)
}
// TODO: [REDACTED_JIRA]
@ -211,7 +211,7 @@ internal class LegacyScanProcessor @Inject constructor(
},
) {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
trackingContextProxy.addContext(scanResponse)
onWalletNotCreated()
navigateTo(
AppRoute.Onboarding(
@ -220,7 +220,7 @@ internal class LegacyScanProcessor @Inject constructor(
),
) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
trackingContextProxy.setContext(scanResponse)
val wasTwinsOnboardingShown =
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.right
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
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.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -34,9 +31,11 @@ class CheckForOnboardingChain(
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
trackingContextProxy.addContext(previousChainResult)
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
@ -45,7 +44,7 @@ class CheckForOnboardingChain(
).left()
}
else -> {
Analytics.setContext(previousChainResult)
trackingContextProxy.setContext(previousChainResult)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()

View file

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

View file

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

View file

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

View file

@ -14,10 +14,11 @@ import com.tangem.common.map
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath
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.configs.CardConfig
import com.tangem.domain.demo.models.DemoConfig
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.StartPrimaryCardLinkingCommand
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
@ -60,7 +61,13 @@ class CreateProductWalletTask(
val cardDto = CardDTO(card)
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() ->
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> {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains.toList()
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.getDemoBlockchains(cardId).toList()
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,6 +15,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -122,6 +123,7 @@ internal object UserWalletsListManagerModule {
passwordRequester: HotWalletPasswordRequester,
appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
tangemHotSdk: TangemHotSdk,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -168,6 +170,7 @@ internal object UserWalletsListManagerModule {
appPreferencesStore = appPreferencesStore,
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
tangemHotSdk = tangemHotSdk,
)
}

View file

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

View file

@ -20,11 +20,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
.mapLatest { listOfNotNull(it.userWallet) }
.distinctUntilChanged()
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { it.userWallet }
@ -46,6 +41,11 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
override val walletsCount: Int
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 {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }

View file

@ -18,6 +18,7 @@ import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
@ -27,6 +28,8 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -44,6 +47,7 @@ internal class DefaultUserWalletsListRepository(
private val savePersistentInformation: ProviderSuspend<Boolean>,
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val tangemHotSdk: TangemHotSdk,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -119,14 +123,19 @@ internal class DefaultUserWalletsListRepository(
}
}
val oldUserWallet = userWalletsSync().find { it.walletId == userWallet.walletId }
if (oldUserWallet != null) {
checkForUpgradeAndDeleteHotWalletIfNeeded(
newUserWallet = userWallet,
oldUserWallet = oldUserWallet,
)
}
// update the userWallets state and add if it doesn't exist
updateWallets { currentWallets ->
val wallets = currentWallets.orEmpty()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
} else {
wallets + userWallet
}
wallets.addOrReplace(userWallet) { it.walletId == userWallet.walletId }
}
// update the selectedUserWallet state if it is the only wallet
@ -149,7 +158,7 @@ internal class DefaultUserWalletsListRepository(
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
runSuspendCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
@ -193,6 +202,8 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds)
removeHotWalletsFromSDK(userWalletIds)
userWallets.update { currentWallets ->
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
selectedUserWallet.update { currentSelected ->
@ -232,7 +243,7 @@ internal class DefaultUserWalletsListRepository(
val encryptionKey = requestPasswordRecursive(
hotWalletId = userWallet.hotWalletId,
block = { password ->
runCatching {
runSuspendCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
}.onFailure {
raise(UnlockWalletError.UnableToUnlock)
@ -287,9 +298,13 @@ internal class DefaultUserWalletsListRepository(
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWallets = userWalletsSync()
val userWalletIds = userWallets.map { it.walletId }.toSet()
val biometricKeys = runCatching {
val userWallets = userWalletsSync().filter { it.isLocked }
if (userWallets.isEmpty()) {
return@either
}
val biometricKeys = runSuspendCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
// TODO handle error properly [REDACTED_TASK_KEY]
@ -309,14 +324,15 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(it)
}
// if we cant unlock all wallets
if (userWalletIds.all { it in unlockedWalletsIds }.not()) {
// if we cant unlock any of the locked wallets, return error
// (isLocked remains `true` here because we haven't updated the wallets yet)
if (unlockedWallets.any { it.isLocked }.not()) {
raise(UnlockWalletError.UnableToUnlock)
}
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
updateWallets { userWallets.updateWith(sensitiveInfo) }
updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
@ -345,6 +361,33 @@ internal class DefaultUserWalletsListRepository(
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 checkForUpgradeAndDeleteHotWalletIfNeeded(
newUserWallet: UserWallet,
oldUserWallet: UserWallet,
) {
if (newUserWallet.walletId == oldUserWallet.walletId &&
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
) {
removeHotWalletsFromSDK(walletIds = listOf(oldUserWallet.walletId))
}
}
private suspend fun removeHotWalletsFromSDK(walletIds: List<UserWalletId>) {
val hotWalletsToDelete = userWalletsSync()
.filterIsInstance<UserWallet.Hot>()
.filter { walletIds.contains(it.walletId) }
hotWalletsToDelete.forEach {
tangemHotSdk.delete(it.hotWalletId)
}
}
private suspend fun requestPasswordRecursive(
hotWalletId: HotWalletId,
block: suspend (CharArray) -> UserWalletEncryptionKey?,

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.ui.UiMessageSender
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.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
@ -35,15 +32,13 @@ import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.staking.FetchStakingOptionsUseCase
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -67,7 +62,7 @@ internal class MainViewModel @Inject constructor(
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
private val blockchainSDKFactory: BlockchainSDKFactory,
private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val fetchStakingOptionsUseCase: FetchStakingOptionsUseCase,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
@GlobalUiMessageSender private val messageSender: UiMessageSender,
private val keyboardValidator: KeyboardValidator,
@ -83,9 +78,10 @@ internal class MainViewModel @Inject constructor(
private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
private val trackingContextProxy: TrackingContextProxy,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -108,7 +104,7 @@ internal class MainViewModel @Inject constructor(
launch { fetchAppCurrenciesUseCase() }
launch { fetchStakingTokens() }
launch { fetchStakingOptions() }
launch { initPushNotifications() }
}
@ -181,36 +177,26 @@ internal class MainViewModel @Inject constructor(
.mapLeft { emptyFlow<UserWallet>() }
.onRight { wallet ->
wallet.distinctUntilChanged()
.onEach { Analytics.setContext(it) }
.onEach { trackingContextProxy.setContext(it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
}
private suspend fun fetchStakingTokens() {
fetchStakingTokensUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.onRight { Timber.d("Staking token list was fetched successfully") }
private suspend fun fetchStakingOptions() {
fetchStakingOptionsUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch staking options") }
.onRight { Timber.d("Staking options were fetched successfully") }
}
private fun initializeOffRamp() {
viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService
sellService.update()
}
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
isLogEnabled = LogConfig.network.moonPayService,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

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

View file

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

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