diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 097bcd4c84..cf4d86ade8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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,7 @@ dependencies { implementation(projects.domain.walletManager.models) implementation(projects.domain.yieldSupply) implementation(projects.domain.blockaid) + implementation(projects.domain.hotWallet) implementation(projects.common) implementation(projects.common.routing) @@ -167,6 +170,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 +213,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 +250,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) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index feebdbf578..57a2c29563 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -63,7 +63,6 @@ abstract class BaseTestCase : TestCase( private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.CAMERA, ) /** diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index 62e83ab9c4..cb7ed9ff51 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -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" } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt index b8751cb679..992a417013 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt @@ -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 ) } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt index 0c3430dc3c..82c7f1feab 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt @@ -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() } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt index c7f2d7f954..bca0130819 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt @@ -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() } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/ReceiveScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/ReceiveScenarios.kt new file mode 100644 index 0000000000..57aff3cecc --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/ReceiveScenarios.kt @@ -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() } + } +} diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt new file mode 100644 index 0000000000..f1d548fd8e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt @@ -0,0 +1,149 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.screens.* +import com.tangem.screens.onMainScreen +import com.tangem.screens.onSendConfirmScreen +import com.tangem.screens.onSendScreen +import com.tangem.screens.onTokenDetailsScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.openSendScreen(tokenName: String, mockState: String = "") { + val scenarioState = mockState.ifEmpty { tokenName } + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Click on 'Send' button") { + onTokenDetailsScreen { sendButton().performClick() } + } +} + +fun BaseTestCase.checkNetworkFeeBlock(currentFeeAmount: String, withFeeSelector: Boolean) { + step("Assert fee selector block is displayed") { + onSendConfirmScreen { feeSelectorBlock.assertIsDisplayed() } + } + step("Assert fee selector icon is displayed") { + onSendConfirmScreen { feeSelectorIcon.assertIsDisplayed() } + } + step("Assert fee selector title is displayed") { + onSendConfirmScreen { feeSelectorTitle.assertIsDisplayed() } + } + step("Assert fee selector tooltip icon is displayed") { + onSendConfirmScreen { feeSelectorTooltipIcon.assertIsDisplayed() } + } + step("Assert fee amount = '$currentFeeAmount'") { + onSendConfirmScreen { feeAmount.assertTextContains(currentFeeAmount) } + } + if (withFeeSelector) { + step("Assert select fee icon is displayed") { + onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() } + } + } +} + +fun BaseTestCase.openSendConfirmScreen( + tokenName: String, + inputAmount: String, + recipientAddress: String +) { + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, inputAmount) + } + step("Type recipient address") { + onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } +} + +fun BaseTestCase.openSendAddressScreen( + tokenName: String, + inputAmount: String +) { + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Click on 'Send' button") { + onTokenDetailsScreen { sendButton().performClick() } + } + step("Type '$inputAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(inputAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Assert 'Send Address' container is displayed") { + onSendAddressScreen { container.assertIsDisplayed() } + } +} + +fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) { + step("Assert 'Back' button is displayed") { + onScanQrScreen { backTopAppBarButton.assertIsDisplayed() } + } + step("Assert 'Flashlight' button is displayed") { + onScanQrScreen { flashlightButton.assertIsDisplayed() } + } + step("Assert 'Gallery' button is displayed") { + onScanQrScreen { galleryButton.assertIsDisplayed() } + } + if (!emptyClipboard) { + step("Assert 'Paste from clipboard' is displayed") { + onScanQrScreen { pasteFromClipboardButton.assertIsDisplayed() } + } + } +} + +fun BaseTestCase.checkDestinationTagBlock(hint: String) { + step("Assert 'Destination Tag' title is displayed") { + onSendAddressScreen { destinationTagBlockTitle().assertIsDisplayed() } + } + step("Assert 'Destination Tag' text is displayed") { + onSendAddressScreen { destinationTagBlockText.assertIsDisplayed() } + } + step("Assert 'Destination Tag' text field hint contains text '$hint'") { + onSendAddressScreen { destinationTagTextFieldHint.assertTextContains(hint) } + } + step("Assert 'Destination Tag' text field is displayed") { + onSendAddressScreen { destinationTagTextField.assertIsDisplayed() } + } + step("Assert 'Destination Tag' caution is displayed") { + onSendAddressScreen { destinationTagBlockCaution.assertIsDisplayed() } + } +} + +fun BaseTestCase.checkRecentAddressItem(address: String, description: String) { + step("Assert recent address '$address' is displayed") { + onSendAddressScreen { + recentAddressItem(address).assertIsDisplayed() + } + } + step("Assert recent address item description is '$description'") { + onSendAddressScreen { + recentAddressItemDescription(description).assertTextContains( + description, + substring = true + ) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt new file mode 100644 index 0000000000..f90275a934 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendWarningScenarios.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ActionIsUnavailableDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ActionIsUnavailableDialogPageObject.kt new file mode 100644 index 0000000000..166e1056b9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ActionIsUnavailableDialogPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt index 4f0037cbab..aba525c895 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt @@ -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 diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CameraAccessDeniedBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CameraAccessDeniedBottomSheetPageObject.kt new file mode 100644 index 0000000000..0f4d726d49 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/CameraAccessDeniedBottomSheetPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 9543830d26..7f6402803d 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -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)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/OperationIsUnavailableDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/OperationIsUnavailableDialogPageObject.kt new file mode 100644 index 0000000000..b4e9fd6f91 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/OperationIsUnavailableDialogPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/PermissionDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/PermissionDialogPageObject.kt new file mode 100644 index 0000000000..4b11d545c3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/PermissionDialogPageObject.kt @@ -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() { + + 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") + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ScanQrPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ScanQrPageObject.kt new file mode 100644 index 0000000000..a7c3dc28ad --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ScanQrPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SellPageObject.kt similarity index 50% rename from app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt rename to app/src/androidTest/kotlin/com/tangem/screens/SellPageObject.kt index d48c6e7e67..ee721654ec 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SellPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { +class SellPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt index 8186ff1683..ec4330c3fd 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt @@ -9,18 +9,135 @@ import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.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(semanticsProvider = semanticsProvider) { + val container: KNode = child { + hasTestTag(SendAddressScreenTestTags.CONTAINER) + useUnmergedTree = true + } + + val addressTextFieldTitle: KNode = child { + hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE) + useUnmergedTree = true + } + val addressTextField: KNode = child { hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD) useUnmergedTree = true } + val addressPasteButton: KNode = child { + hasTestTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON) + useUnmergedTree = true + } + + val qrButton: KNode = child { + hasTestTag(SendAddressScreenTestTags.QR_BUTTON) + useUnmergedTree = true + } + + val clearTextFieldButton: KNode = child { + hasContentDescription(getResourceString(CoreUiR.string.common_close)) + useUnmergedTree = true + } + + val resolvedAddress: KNode = child { + hasTestTag(SendAddressScreenTestTags.RESOLVED_ADDRESS) + } + + val recentAddressesTitle: KNode = child { + hasText(getResourceString(CoreUiR.string.send_recent_transactions)) + useUnmergedTree = true + } + + val myWalletsTitle: KNode = child { + hasText(getResourceString(CoreUiR.string.send_recipient_wallets_title)) + useUnmergedTree = true + } + + fun recentAddressItem(recipientAddress: String, isMyWallet: Boolean = false): KNode = child { + hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM) + hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON)) + hasAnyDescendant(withText(recipientAddress)) + hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT)) + useUnmergedTree = true + if (isMyWallet) { + hasAnySibling(withText(getResourceString(CoreUiR.string.send_recipient_wallets_title))) + hasAnyDescendant(withText(getResourceString(CoreUiR.string.manage_tokens_network_selector_wallet))) + } else { + hasAnySibling(withText(getResourceString(CoreUiR.string.send_recent_transactions))) + hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TRANSACTION_ICON)) + } + } + + fun recentAddressItemDescription(recipientAddress: String): KNode = recentAddressItem(recipientAddress).child { + hasParent(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT)) + useUnmergedTree = true + } + + fun destinationTagBlockTitle(isMemoCorrectOrEmpty: Boolean = true): KNode = child { + hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD_TITLE) + useUnmergedTree = true + if(isMemoCorrectOrEmpty) { + hasText(getResourceString(CoreUiR.string.send_destination_tag_field)) + } else { + hasText(getResourceString(CoreUiR.string.send_memo_destination_tag_error)) + } + } + + val destinationTagTextField: KNode = child { + hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD) + useUnmergedTree = true + } + + val destinationTagTextFieldHint: KNode = child { + hasParent(withTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)) + useUnmergedTree = true + } + + val destinationTagBlockText: KNode = child { + hasText( + getResourceString(CoreUiR.string.send_recipient_memo_footer_v2) + "\n" + + getResourceString(CoreUiR.string.send_recipient_memo_footer_v2_highlighted) + ) + useUnmergedTree = true + } + + val destinationTagBlockCaution: KNode = child { + hasText( + getResourceString( + CoreUiR.string.send_recipient_memo_footer_v2, + CoreUiR.string.send_recipient_memo_footer_v2_highlighted + ), + substring = true + ) + useUnmergedTree = true + } + + val destinationTagPasteButton: KNode = child { + hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_PASTE_BUTTON) + useUnmergedTree = true + } + + val clearDestinationTagTextFieldButton: KNode = child { + hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON) + useUnmergedTree = true + } + val nextButton: KNode = child { + 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 } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt index 5821029a67..8beeddb2eb 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt @@ -2,36 +2,55 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase -import com.tangem.core.ui.R -import com.tangem.core.ui.test.BaseButtonTestTags -import com.tangem.core.ui.test.NotificationTestTags -import com.tangem.core.ui.test.SendConfirmScreenTestTags -import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.test.* +import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString import androidx.compose.ui.test.hasText as withText -import com.tangem.common.ui.R as CommonUiR +import androidx.compose.ui.test.hasTestTag as withTestTag +import com.tangem.core.ui.R as CoreUiR class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { - val title: KNode = child { + val appBarTitle: KNode = child { hasTestTag(TopAppBarTestTags.TITLE) useUnmergedTree = true } val sendButton: KNode = child { - hasTestTag(BaseButtonTestTags.TEXT) - hasText(getResourceString(R.string.common_send)) + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyDescendant(withText(getResourceString(R.string.common_send))) useUnmergedTree = true } + val primaryAmount: KNode = child { + hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT) + useUnmergedTree = true + } - val minimumSendAmountErrorTitle: KNode = child { + val secondaryAmount: KNode = child { + hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + fun leaveDepositButton(amount: String): KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyDescendant(withText(getResourceString(R.string.send_notification_leave_button, amount))) + useUnmergedTree = true + } + + fun reduceAmountButton(amount: String): KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyDescendant(withText(getResourceString(R.string.send_notification_reduce_by, amount))) + useUnmergedTree = true + } + + fun warningTitle(title: String): KNode = child { hasTestTag(NotificationTestTags.TITLE) - hasText(getResourceString(CommonUiR.string.send_notification_invalid_amount_title)) + hasText(title) useUnmergedTree = true } @@ -40,34 +59,76 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider useUnmergedTree = true } - fun minimumSendAmountErrorIcon(amount: String): KNode = child { - hasAnySibling( - withText( - getResourceString( - CommonUiR.string.send_notification_invalid_minimum_amount_text, - amount, - amount, - ) - ) - ) + fun sendWarningIcon(message: String): KNode = child { hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withText(message)) useUnmergedTree = true } - fun minimumSendAmountErrorMessage( - amount: String, + fun sendWarningMessage( + message: String, ): KNode = child { hasTestTag(NotificationTestTags.MESSAGE) - hasText( - getResourceString( - CommonUiR.string.send_notification_invalid_minimum_amount_text, - amount, - amount, - ) - ) + hasText(message) useUnmergedTree = true } + fun warningMessage(messageResId: Int): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText(getResourceString(messageResId)) + useUnmergedTree = true + } + + fun warningIcon(message: String): KNode = child { + hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withText(message)) + useUnmergedTree = true + } + + fun recipientAddress(recipientAddress: String): KNode = child { + hasTestTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS) + hasText(recipientAddress) + useUnmergedTree = true + } + + val blockchainAddress: KNode = child { + hasTestTag(SendConfirmScreenTestTags.BLOCKCHAIN_ADDRESS) + useUnmergedTree = true + } + + val feeSelectorBlock: KNode = child { + hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK) + } + + val feeSelectorIcon: KNode = child { + hasTestTag(FeeSelectorBlockTestTags.ICON) + useUnmergedTree = true + } + + val feeSelectorTitle: KNode = child { + hasTestTag(FeeSelectorBlockTestTags.TITLE) + useUnmergedTree = true + } + + val feeSelectorTooltipIcon: KNode = child { + hasTestTag(FeeSelectorBlockTestTags.TOOLTIP_ICON) + useUnmergedTree = true + } + + val selectFeeIcon: KNode = child { + hasTestTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON) + useUnmergedTree = true + } + + val feeAmount: KNode = child { + hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT)) + useUnmergedTree = true + } + + val refreshButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(CoreUiR.string.warning_button_refresh)) + } } internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt index d12952956b..1e1f1da9d2 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt @@ -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) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapIsNotSupportedDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapIsNotSupportedDialogPageObject.kt new file mode 100644 index 0000000000..2f897e61fd --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapIsNotSupportedDialogPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 1c3fa1fbb1..dff98cfcee 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -97,25 +97,31 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide ) @OptIn(ExperimentalTestApi::class) - val swapButton: LazyListItemNode = horizontalActionChips.childWith { + fun receiveButton(): LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_receive)) + } + + @OptIn(ExperimentalTestApi::class) + fun swapButton(): LazyListItemNode = horizontalActionChips.childWith { hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasText(getResourceString(R.string.common_swap)) } @OptIn(ExperimentalTestApi::class) - val sellButton: LazyListItemNode = horizontalActionChips.childWith { + fun sellButton(): LazyListItemNode = horizontalActionChips.childWith { hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasText(getResourceString(R.string.common_sell)) } @OptIn(ExperimentalTestApi::class) - val buyButton: LazyListItemNode = horizontalActionChips.childWith { + fun buyButton(): LazyListItemNode = horizontalActionChips.childWith { hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasText(getResourceString(R.string.common_buy)) } @OptIn(ExperimentalTestApi::class) - val sendButton: LazyListItemNode = horizontalActionChips.childWith { + fun sendButton(): LazyListItemNode = horizontalActionChips.childWith { hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasText(getResourceString(R.string.common_send)) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BlockchainTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BlockchainTest.kt index 28b46d345c..df177bb300 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BlockchainTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BlockchainTest.kt @@ -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() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index da13bb553c..a618a5b1dc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -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() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 210063dedb..f6cd0fc1e3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -15,7 +15,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.store import dagger.hilt.android.testing.HiltAndroidTest -import io.qameta.allure.kotlin.Allure import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName import org.junit.Test @@ -73,7 +72,7 @@ class FeedbackTest : BaseTestCase() { onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } } step("Click 'Send' button") { - onTokenDetailsScreen { sendButton.performClick() } + onTokenDetailsScreen { sendButton().performClick() } } step("Type '$sendAmount' in input text field") { onSendScreen { @@ -130,7 +129,7 @@ class FeedbackTest : BaseTestCase() { MockProvider.resetEmulateError() } ).run { - Allure.step("Click on 'Accept' button") { + step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } } step("Set scanning error") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt index 014b42a242..d237730598 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt @@ -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() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt index 60dd5885f1..ae35d70e6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt @@ -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() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt index 8601d27658..0f72322a4e 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -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 @@ -121,7 +126,6 @@ class MainScreenActionButtonsTest : BaseTestCase() { @Test fun clickOnBuyButtonTest() { val tokenTitle = "Bitcoin" - val tokenSymbol = "BTC" setupHooks().run { step("Open 'Main Screen'") { @@ -160,12 +164,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 +279,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 +317,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 +366,251 @@ class MainScreenActionButtonsTest : BaseTestCase() { } } } + + @AllureId("895") + @DisplayName("Action buttons: check blockchain information by click on 'Buy' button") + @Test + fun checkClickOnBuyButtonOnMainTest() { + val cardType: MockContent = TwinsMockContent + val cardName = "Twin" + val tokenTitle = "Bitcoin" + + setupHooks().run { + step("Open 'Main Screen' on '$cardName' card") { + openMainScreen(mockContent = cardType, isTwinsCard = true) + } + step("Assert 'Buy' button is displayed") { + onMainScreen { buyButton.assertIsDisplayed() } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.performClick() } + } + step("Click on 'Confirm' button in 'Dialog'") { + waitForIdle() + onDialog { confirmButton.clickWithAssertion() } + } + step("Assert top app bar title contains '$tokenTitle'") { + onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") } + } + step("Assert fiat currency text field is displayed") { + onBuyTokenDetailsScreen { fiatAmountTextField.assertIsDisplayed() } + } + step("Assert fiat currency icon is displayed") { + onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() } + } + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD)) + @AllureId("4395") + @DisplayName("Action buttons (main screen): click on buttons with success response") + @Test + fun clickOnActionButtonsWithSuccessResponseTest() { + val tokenTitle = "Ethereum" + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert 'Buy' button is displayed") { + onMainScreen { buyButton.assertIsDisplayed() } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.performClick() } + } + step("Assert 'Buy' screen title is displayed") { + onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() } + } + step("Assert token with title: '$tokenTitle' is displayed") { + onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Assert 'Swap' button is displayed") { + onMainScreen { swapButton.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onMainScreen { swapButton.performClick() } + } + step("Click on close button on stories screen") { + onSwapStoriesScreen { closeButton.performClick() } + } + step("Assert 'Swap' token screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Assert 'Sell' button is displayed") { + onMainScreen { sellButton.assertIsDisplayed() } + } + step("Click on 'Sell' button") { + onMainScreen { sellButton.performClick() } + } + step("Assert 'Sell' token screen title is displayed") { + onSellScreen { title.assertIsDisplayed() } + } + } + } + + @AllureId("4396") + @DisplayName("Action buttons (main screen): click on buttons without data") + @Test + fun clickOnActionButtonsWithoutDataTest() { + setupHooks( + additionalAfterSection = { + enableWiFi() + enableMobileData() + } + ).run { + step("Turn off internet") { + disableWiFi() + disableMobileData() + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Assert 'Buy' button is displayed") { + onMainScreen { buyButton.assertIsDisplayed() } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + step("Assert 'Swap' button is displayed") { + onMainScreen { swapButton.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onMainScreen { swapButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + step("Assert 'Sell' button is displayed") { + onMainScreen { sellButton.assertIsDisplayed() } + } + step("Click on 'Sell' button") { + onMainScreen { sellButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + } + } + + @AllureId("4398") + @DisplayName("Action buttons (main screen): click on buttons with error response") + @Test + fun clickOnActionButtonsWithErrorResponseTest() { + val scenarioName = "express_api_assets" + val scenarioState = "Error" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName, scenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Assert 'Buy' button is displayed") { + onMainScreen { buyButton.assertIsDisplayed() } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + step("Assert 'Swap' button is displayed") { + onMainScreen { swapButton.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onMainScreen { swapButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + step("Assert 'Sell' button is displayed") { + onMainScreen { sellButton.assertIsDisplayed() } + } + step("Click on 'Sell' button") { + onMainScreen { sellButton.performClick() } + } + step("Check 'Action is unavailable' dialog") { + checkActionIsUnavailableDialog() + } + step("Click on 'Ok' button") { + onDialog { okButton.performClick() } + } + } + } + + @AllureId("3642") + @DisplayName("Action buttons (main screen): check buttons state") + @Test + fun checkButtonsStateTest() { + val scenarioName = "user_tokens_api" + val scenarioState = "EmptyTokensList" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Assert action buttons is not enabled") { + assertActionButtonsForMultiCurrencyWallet(isEnabled = false) + } + step("Reset Wiremock scenario: '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + step("Perform pull to refresh") { + pullToRefresh(steps = 10) + waitForIdle() + } + step("Assert action buttons is enabled") { + assertActionButtonsForMultiCurrencyWallet(isEnabled = true) + } + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + step("Perform pull to refresh") { + pullToRefresh(steps = 10) + waitForIdle() + } + step("Assert action buttons is not enabled") { + assertActionButtonsForMultiCurrencyWallet(isEnabled = false) + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt new file mode 100644 index 0000000000..a531a3cc1a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/MyWalletsBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/MyWalletsBlockTest.kt new file mode 100644 index 0000000000..13d2d3d727 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/MyWalletsBlockTest.kt @@ -0,0 +1,137 @@ +package com.tangem.tests.send.addressScreen + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.DOGECOIN_ADDRESS +import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSendAddressScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onSendAddressScreen +import com.tangem.screens.onSendConfirmScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class MyWalletsBlockTest : BaseTestCase() { + private val txHistoryScenarioName = "dogecoin_tx_history" + + @AllureId("4579") + @DisplayName("Send (address screen): click on 'My wallet address'") + @Test + fun clickOnMyWalletAddressTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "OutgoingTransaction" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Click on 'My wallet' address") { + onSendAddressScreen { + recentAddressItem(recipientAddress = DOGECOIN_ADDRESS, isMyWallet = true).performClick() + } + } + step("Assert recipient address is displayed on confirm screen") { + onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() } + } + } + } + + @AllureId("4599") + @DisplayName("Send (address screen): 'My wallets' block is not displayed for account networks") + @Test + fun myWalletsBlockForAccountNetworksNotDisplayedTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'My wallets' title is not displayed") { + onSendAddressScreen { myWalletsTitle.assertIsNotDisplayed() } + } + step("Assert recent address item is not displayed") { + onSendAddressScreen { recentAddressItem(ETHEREUM_ADDRESS).assertIsNotDisplayed() } + } + } + } + + @AllureId("4737") + @DisplayName("Send (address screen): assert 'My wallet address is correct'") + @Test + fun checkMyWalletsBlockTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "OutgoingTransaction" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'My wallets' title is displayed") { + onSendAddressScreen { myWalletsTitle.assertIsDisplayed() } + } + step("Assert 'My wallet' address is displayed") { + onSendAddressScreen { + recentAddressItem(recipientAddress = DOGECOIN_ADDRESS, isMyWallet = true).performClick() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt new file mode 100644 index 0000000000..0c0de9d580 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt @@ -0,0 +1,330 @@ +package com.tangem.tests.send.addressScreen + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.DOGECOIN_ADDRESS +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.SwipeDirection +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeVertical +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.* +import com.tangem.screens.onSendAddressScreen +import com.tangem.screens.onSendConfirmScreen +import com.tangem.screens.onSendScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class RecentBlockTest : BaseTestCase() { + private val txHistoryScenarioName = "dogecoin_tx_history" + private val descriptionForWalletTransaction = "DOGE 0.0192, 11/10/2025, 1:46 PM" + + @AllureId("4005") + @DisplayName("Send (address screen): check address history") + @Test + fun sendAddressHistoryTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "OutgoingTransaction" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Click on previous address") { + onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() } + } + step("Assert recipient address is displayed") { + onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() } + } + step("Press system 'Back' button") { + device.uiDevice.pressBack() + } + step("Assert address text field contains correct recipient address") { + onSendAddressScreen { addressTextField.assertTextContains(DOGECOIN_ADDRESS) } + } + step("Assert 'Recent' title is not displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() } + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + step("Click on 'Cross' button") { + onSendAddressScreen { clearTextFieldButton.clickWithAssertion() } + } + step("Assert recipient text field is empty") { + onSendAddressScreen { addressTextField.assertTextEquals("") } + } + step("Assert 'Recent' title is displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + } + } + + @AllureId("4597") + @DisplayName("Send (address screen): check 'Recent' block, transaction history doesn't supported") + @Test + fun recentBlockTransactionHistoryDoesNotSupportedTest() { + val tokenName = "Polkadot" + val sendAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + step("Open 'Send Screen' with token: $tokenName") { + openSendScreen(tokenName) + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Assert 'Send Address' container is displayed") { + onSendAddressScreen { container.assertIsDisplayed() } + } + step("Assert 'Recent' title is not displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() } + } + } + } + + @AllureId("4596") + @DisplayName("Send (address screen): check 'Recent' block, transaction history is empty") + @Test + fun recentBlockTransactionHistoryEmptyTest() { + val tokenName = "POL (ex-MATIC)" + val sendAmount = "1" + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'Recent' title is not displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() } + } + step("Assert recent address item is not displayed") { + onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).assertIsNotDisplayed() } + } + } + } + + @AllureId("4596") + @DisplayName("Send (address screen): check 'Recent' block, transaction history does not load") + @Test + fun recentBlockTransactionHistoryDoesNotLoadTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "Error" + val scenarioState = "Dogecoin" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'Recent' title is not displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() } + } + step("Assert recent address item is not displayed") { + onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).assertIsNotDisplayed() } + } + } + } + + @AllureId("4572") + @DisplayName("Send (address screen): check 'Recent' block for >10 transactions") + @Test + fun checkRecentBlockMoreThanTenTransactionsTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "11OutgoingTransactions" + val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq" + val shortenedRecipientAddress = "DJ2TaZ5vvp3mBLugU...Li4uYaq123456789b" + val descriptionForTransaction = "DOGE 0.10, 11/10/2025, 1:46 PM" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'Recent' title is displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() } + } + step("Check recent address item №1") { + checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction) + } + step("Check recent address item №2") { + checkRecentAddressItem(address = shortenedRecipientAddress, description = descriptionForTransaction) + } + step("Check recent address item №3") { + checkRecentAddressItem(address = recipientAddressBase + "k", description = descriptionForTransaction) + } + step("Check recent address item №4") { + checkRecentAddressItem(address = recipientAddressBase + "c", description = descriptionForTransaction) + } + step("Check recent address item №5") { + checkRecentAddressItem(address = recipientAddressBase + "d", description = descriptionForTransaction) + } + step("Check recent address item №6") { + checkRecentAddressItem(address = recipientAddressBase + "e", description = descriptionForTransaction) + } + step("Swipe up") { + swipeVertical(SwipeDirection.UP) + } + step("Check recent address item №7") { + checkRecentAddressItem(address = recipientAddressBase + "f", description = descriptionForTransaction) + } + step("Check recent address item №8") { + checkRecentAddressItem(address = recipientAddressBase + "g", description = descriptionForTransaction) + } + step("Check recent address item №9") { + checkRecentAddressItem(address = recipientAddressBase + "o", description = descriptionForTransaction) + } + step("Check recent address item №10") { + checkRecentAddressItem(address = recipientAddressBase + "p", description = descriptionForTransaction) + } + step("Assert recent address item №11 is not displayed") { + onSendAddressScreen { recentAddressItem(recipientAddressBase + "a").assertIsNotDisplayed() } + } + step("Click on recent address item №1") { + onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() } + } + step("Assert recipient address is displayed on 'Send Confirm' screen") { + onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() } + } + } + } + + @AllureId("4676") + @DisplayName("Send (address screen): check 'Recent' block for <10 transactions") + @Test + fun checkRecentBlockLessThanTenTransactionsTest() { + val tokenName = "Dogecoin" + val sendAmount = "1" + val txHistoryScenarioState = "2OutgoingTransactions" + val descriptionForWalletTransaction2 = "DOGE 0.0192, 11/09/2025, 3:18 PM" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(txHistoryScenarioName) + } + ).run { + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName) + } + step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") { + setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Assert 'Recent' title is displayed") { + onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() } + } + step("Check recent address item") { + checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction) + } + step("Check recent address item") { + checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = descriptionForWalletTransaction2) + } + step("Click on recent address item") { + onSendAddressScreen { recentAddressItem(DOGECOIN_ADDRESS).performClick() } + } + step("Assert recipient address is displayed on 'Send Confirm' screen") { + onSendConfirmScreen { recipientAddress(DOGECOIN_ADDRESS).assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt new file mode 100644 index 0000000000..6529446443 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt @@ -0,0 +1,583 @@ +package com.tangem.tests.send.addressScreen + +import android.Manifest +import androidx.compose.ui.test.hasText +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS +import com.tangem.common.constants.TestConstants.ENS_NAME +import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS +import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.XRP_X_ADDRESS +import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.XRP_X_RECIPIENT_ADDRESS_WITH_TAG +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.clearClipboard +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setClipboardText +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.core.ui.R +import com.tangem.scenarios.* +import com.tangem.screens.* +import com.tangem.wallet.BuildConfig +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SendAddressScreenTest : BaseTestCase() { + + @AllureId("4004") + @DisplayName("Send (address screen): check destination tag") + @Test + fun sendDestinationTagTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val correctMemo = "123" + val invalidMemo = "hz" + val xrpRecipientAddress = XRP_RECIPIENT_ADDRESS + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val context = device.context + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Set clipboard text") { + setClipboardText(context, correctMemo) + } + step("Assert 'Destination Tag' title is displayed") { + onSendAddressScreen { destinationTagBlockTitle().assertIsDisplayed() } + } + step("Assert 'Destination Tag' text is displayed") { + onSendAddressScreen { destinationTagBlockText.assertIsDisplayed() } + } + step("CLick on 'Paste' button") { + onSendAddressScreen { destinationTagPasteButton.clickWithAssertion() } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) } + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + step("Click on 'Clear text field button'") { + onSendAddressScreen { clearDestinationTagTextFieldButton.clickWithAssertion() } + } + step("Type invalid memo in input text field") { + onSendAddressScreen { destinationTagTextField.performTextReplacement(invalidMemo) } + } + step("Assert 'Invalid memo' title is displayed") { + onSendAddressScreen { destinationTagBlockTitle(isMemoCorrectOrEmpty = false).assertIsDisplayed() } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + } + } + + @AllureId("4543") + @DisplayName("Send (address screen): check address field") + @Test + fun sendAddressFieldTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS + val invalidAddress = "s" + val walletAddress = ETHEREUM_ADDRESS + val context = device.context + val recipient = getResourceString(R.string.send_recipient) + val notAValidAddress = getResourceString(R.string.send_recipient_address_error) + val sameAsWalletAddress = getResourceString(R.string.send_error_address_same_as_wallet) + + setupHooks( + additionalAfterSection = { + clearClipboard() + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Set clipboard text") { + setClipboardText(context, recipientAddress) + } + step("Click on 'Paste' button") { + onSendAddressScreen { addressPasteButton.clickWithAssertion() } + } + step("Assert address text field contains correct recipient address") { + onSendAddressScreen { addressTextField.assertTextContains(recipientAddress) } + } + step("Assert address text field title is displayed") { + onSendAddressScreen { addressTextFieldTitle.assertTextContains(recipient) } + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + step("Set clipboard text") { + setClipboardText(context, invalidAddress) + } + step("Click on 'Cross' button") { + onSendAddressScreen { clearTextFieldButton.clickWithAssertion() } + } + step("Click on 'Paste' button") { + onSendAddressScreen { addressPasteButton.clickWithAssertion() } + } + step("Assert address text field contains invalid address") { + onSendAddressScreen { addressTextField.assertTextContains(invalidAddress) } + } + step("Assert invalid address text field title is displayed") { + onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + step("Set clipboard text") { + setClipboardText(context, walletAddress) + } + step("Click on 'Cross' button") { + onSendAddressScreen { clearTextFieldButton.clickWithAssertion() } + } + step("Click on 'Paste' button") { + onSendAddressScreen { addressPasteButton.clickWithAssertion() } + } + step("Assert address text field contains invalid address") { + onSendAddressScreen { addressTextField.assertTextContains(walletAddress) } + } + step("Assert 'Address is the same as wallet address' error title is displayed") { + onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + } + } + + @AllureId("4004") + @DisplayName("Send (address screen): check ENS name") + @Test + fun sendEnsNameTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + val ensName = ENS_NAME + val invalidEnsName = "l" + val ensAddress = ENS_ETHEREUM_RECIPIENT_ADDRESS + val ensShortenedAddress = ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS + val scenarioName = "eth_call_api" + val scenarioState = "EnsName" + val notAValidAddress = getResourceString(R.string.send_recipient_address_error) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() } + } + step("Click on 'Send' button") { + onTokenDetailsScreen { sendButton().performClick() } + } + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type ENS name: '$ensName' in text field") { + onSendAddressScreen { addressTextField.performTextReplacement(ensName) } + } + step("Assert ENS address displayed") { + onSendAddressScreen { resolvedAddress.assertTextContains(ensAddress, substring = true) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert recipient address is displayed") { + onSendConfirmScreen { recipientAddress(ensName).assertIsDisplayed() } + } + step("Assert blockchain address is displayed") { + onSendConfirmScreen { blockchainAddress.assertTextContains(ensShortenedAddress, substring = true) } + } + step("Click on recipient address") { + onSendConfirmScreen { recipientAddress(ensName).performClick() } + } + step("Click on 'Cross' button") { + onSendAddressScreen { clearTextFieldButton.clickWithAssertion() } + } + step("Type invalid ENS name: '$invalidEnsName' in text field") { + onSendAddressScreen { addressTextField.performTextReplacement(invalidEnsName) } + } + step("Assert invalid address error title is displayed") { + onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) } + } + } + } + + @AllureId("4574") + @DisplayName("Send (address screen): check scan QR code screen") + @Test + fun checkQrCodeScreenTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + val packageName = BuildConfig.APPLICATION_ID + val permissionName = Manifest.permission.CAMERA + + setupHooks( + additionalBeforeSection = { + device.uiDevice.executeShellCommand("pm grant $packageName $permissionName") + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Click on 'QR' button") { + onSendAddressScreen { qrButton.clickWithAssertion() } + } + step("Check 'Scan QR code' screen") { + checkScanQrScreen() + } + } + } + + @AllureId("4576") + @DisplayName("Send (address screen): give camera permission") + @Test + fun giveCameraPermissionScreenTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Click on 'QR' button") { + onSendAddressScreen { qrButton.clickWithAssertion() } + } + step("Assert 'While using app' button is displayed") { + waitForIdle() + PermissionDialogPageObject { allowWhileUsingButton.isDisplayed() } + } + step("Assert 'Allow only this time' button is displayed") { + PermissionDialogPageObject { allowOnlyThisTimeButton.isDisplayed() } + } + step("Assert 'Don't allow' button is displayed") { + PermissionDialogPageObject { doNotAllowButton.isDisplayed() } + } + step("CLick on 'While using app' button") { + PermissionDialogPageObject { allowWhileUsingButton.click() } + } + step("Check 'Scan QR code' screen") { + checkScanQrScreen() + } + } + } + + @AllureId("4578") + @DisplayName("Send (address screen): don't give camera permission") + @Test + fun doNotGiveCameraPermissionScreenTest() { + val tokenName = "Ethereum" + val sendAmount = "1" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Click on 'QR' button") { + onSendAddressScreen { qrButton.clickWithAssertion() } + } + step("Click on 'Don't allow' button") { + waitForIdle() + PermissionDialogPageObject { doNotAllowButton.click() } + } + step("Assert 'Camera access denied' bottom sheet title is displayed") { + waitForIdle() + onCameraAccessDeniedBottomSheet { title.assertIsDisplayed() } + } + step("Assert 'Camera access denied' bottom sheet subtitle is displayed") { + onCameraAccessDeniedBottomSheet { subtitle.assertIsDisplayed() } + } + step("Assert 'Settings' button is displayed") { + onCameraAccessDeniedBottomSheet { settingsButton.assertIsDisplayed() } + } + step("Assert 'Select from the gallery' button is displayed") { + onCameraAccessDeniedBottomSheet { selectFromGalleryButton.assertIsDisplayed() } + } + step("Assert 'Close' button is displayed") { + onCameraAccessDeniedBottomSheet { closeButton.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onCameraAccessDeniedBottomSheet { closeButton.performClick() } + } + step("Assert 'Address' screen is displayed") { + onSendAddressScreen { container.assertIsDisplayed() } + } + } + } + + @AllureId("4590") + @DisplayName("Send (address screen): enter 'r' address (XRP)") + @Test + fun xrpEnterRAddressTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val xrpRecipientAddress = XRP_RECIPIENT_ADDRESS + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val optionalText = getResourceString(R.string.send_optional_field) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) } + } + step("Check 'Destination tag' block") { + checkDestinationTagBlock(hint = optionalText) + } + } + } + + @AllureId("4591") + @DisplayName("Send (address screen): enter 'X' address without tag (XRP)") + @Test + fun xrpEnterXAddressWithoutTagTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) } + } + step("Check 'Destination tag' block") { + checkDestinationTagBlock(hint = tagAlreadyIncluded) + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + } + } + + @AllureId("4592") + @DisplayName("Send (address screen): enter 'X' address with tag (XRP)") + @Test + fun xrpEnterXAddressWithTagTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS_WITH_TAG + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) } + } + step("Check 'Destination tag' block") { + checkDestinationTagBlock(hint = tagAlreadyIncluded) + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + } + } + + @AllureId("4594") + @DisplayName("Send (address screen): enter own address (XRP)") + @Test + fun xrpEnterOwnXAddressTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val walletAddress = XRP_X_ADDRESS + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val sameAsWalletAddress = getResourceString(R.string.send_error_address_same_as_wallet) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(walletAddress) } + } + step("Assert 'Address is the same as wallet address' error title is displayed") { + onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) } + } + step("Assert 'Next' button is disabled") { + onSendAddressScreen { nextButton.assertIsNotEnabled() } + } + } + } + + @AllureId("4593") + @DisplayName("Send (address screen): enter tag before address (XRP)") + @Test + fun xrpEnterTagBeforeAddressTest() { + val tokenName = "XRP Ledger" + val sendAmount = "1" + val xrpRecipientAddress = XRP_X_RECIPIENT_ADDRESS + val destinationTag = "12345" + val emptyText = "" + val userTokensScenarioName = "user_tokens_api" + val userTokensScenarioState = "XRP" + val tagAlreadyIncluded = getResourceString(R.string.send_additional_field_already_included) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenarioName) + } + ).run { + step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") { + setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Send Address' screen") { + openSendAddressScreen(tokenName, sendAmount) + } + step("Type destination tag in input text field") { + onSendAddressScreen { destinationTagTextField.performTextReplacement(destinationTag) } + } + step("Type X address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) } + } + step("Destination tag text field doesn't contains destination tag: '$destinationTag'") { + onSendAddressScreen { destinationTagTextField.assert (!hasText(destinationTag)) } + } + step("Destination tag text field cleared") { + onSendAddressScreen { destinationTagTextField.assertTextEquals(emptyText) } + } + step("Check 'Destination tag' block") { + checkDestinationTagBlock(hint = tagAlreadyIncluded) + } + step("Assert 'Next' button is enabled") { + onSendAddressScreen { nextButton.assertIsEnabled() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt new file mode 100644 index 0000000000..5a89153081 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt @@ -0,0 +1,300 @@ +package com.tangem.tests.send.confirmScreen + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.core.ui.R +import com.tangem.scenarios.* +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SendConfirmScreenTest : BaseTestCase() { + + @AllureId("4003") + @DisplayName("Send (Confirm screen): change sending amount") + @Test + fun changeSendingAmountTest() { + val tokenName = "Ethereum" + val inputAmount = "1" + val newInputAmount = "0.9" + val ethereumAmount = "1.00" + val fiatAmount = "$2,535.63" + val newFiatAmount = "$2,282.07" + val newEthereumAmount = "0.90" + val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Click on 'Send' button") { + onTokenDetailsScreen { sendButton().performClick() } + } + step("Type '$inputAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount = '$fiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type recipient address") { + onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert token amount = '$ethereumAmount'") { + onSendConfirmScreen { primaryAmount.assertTextContains(ethereumAmount) } + } + step("Assert fiat amount = '$fiatAmount'") { + onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) } + } + step("Click on token amount") { + onSendConfirmScreen { primaryAmount.performClick() } + } + step("Clear input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextClearance() + } + } + step("Type '$newInputAmount' in input text field") { + onSendScreen { + amountInputTextField.performTextReplacement(newInputAmount) + } + } + step("Assert fiat amount = '$newFiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(newFiatAmount) } + } + step("Click on 'Continue' button") { + onSendScreen { continueButton.clickWithAssertion() } + } + step("Assert token amount = '$newEthereumAmount'") { + onSendConfirmScreen { primaryAmount.assertTextContains(newEthereumAmount) } + } + step("Assert fiat amount = '$newFiatAmount'") { + onSendConfirmScreen { secondaryAmount.assertTextContains(newFiatAmount) } + } + } + } + + @AllureId("552") + @DisplayName("Send (Confirm screen): switch to equivalent") + @Test + fun switchToEquivalentTest() { + val tokenName = "Ethereum" + val inputAmount = "1" + val tokenAmount = "1.00" + val ethereumAmount = "ETH 1.00" + val fiatAmount = "$2,535.63" + val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Click on 'Send' button") { + onTokenDetailsScreen { sendButton().performClick() } + } + step("Type '$inputAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount = '$fiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) } + } + step("Click on exchange icon") { + onSendScreen { exchangeIcon.performClick() } + } + step("Assert token amount = '$ethereumAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(ethereumAmount) } + } + step("Click on exchange icon") { + onSendScreen { exchangeIcon.performClick() } + } + step("Assert fiat amount = '$fiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type recipient address") { + onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert primary amount = '$tokenAmount'") { + onSendConfirmScreen { primaryAmount.assertTextContains(tokenAmount) } + } + step("Assert secondary amount = '$fiatAmount'") { + onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) } + } + } + } + + @AllureId("553") + @DisplayName("Send (Confirm screen): check fee for blockchain with unknown fee") + @Test + fun checkFeeForBlockchainWithUnknownFeeTest() { + val tokenName = "Polygon" + val inputAmount = "1" + val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS + val currentFeeAmount = "<$0.01" + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Swipe up to token") { + swipeVertical(SwipeDirection.UP) + } + step("Open 'Send Confirm' screen for token '$tokenName'") { + openSendConfirmScreen( + tokenName = tokenName, + inputAmount = inputAmount, + recipientAddress = recipientAddress + ) + } + step("Check network fee block") { + checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true) + } + } + } + + @AllureId("4565") + @DisplayName("Send (Confirm screen): check fee for token with unknown fee") + @Test + fun checkFeeForTokenWithUnknownFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "1" + val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS + val currentFeeAmount = "<$0.01" + val scenarioName = "eth_estimate_gas" + val scenarioState = "UnknownFee" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Swipe up to token") { + swipeVertical(SwipeDirection.UP) + } + step("Open 'Send Confirm' screen for token '$tokenName'") { + openSendConfirmScreen( + tokenName = tokenName, + inputAmount = inputAmount, + recipientAddress = recipientAddress + ) + } + step("Check network fee block") { + checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true) + } + } + } + + @AllureId("554") + @DisplayName("Send (Confirm screen): check fee warning") + @Test + fun checkFeeWarningTest() { + val tokenName = "Polkadot" + val tokenAmount = "0.1" + val warningTitle = getResourceString(R.string.send_fee_unreachable_error_title) + val warningMessageResId = R.string.send_fee_unreachable_error_text + val currentFeeAmount = "$0.05" + + setupHooks( + additionalAfterSection = { + enableWiFi() + enableMobileData() + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + step("Open 'Send Screen' with token: $tokenName") { + openSendScreen(tokenName) + } + step("Type '$tokenAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(tokenAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) } + } + step("Turn off internet") { + disableWiFi() + disableMobileData() + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Turn on internet") { + enableWiFi() + enableMobileData() + } + step("Assert 'Network fee info unreachable' warning title is displayed") { + onSendConfirmScreen { warningTitle(warningTitle).assertIsDisplayed() } + } + step("Assert 'Check your internet connection' warning message is displayed") { + onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() } + } + step("Assert warning icon is displayed") { + onSendConfirmScreen { warningIcon(warningTitle).assertIsDisplayed() } + } + step("Click on 'Refresh' button") { + waitForIdle() + onSendConfirmScreen { refreshButton.clickWithAssertion() } + } + step("Check network fee block") { + checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = false) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/AzeroWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/AzeroWarningsTest.kt new file mode 100644 index 0000000000..3e4c62024e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/AzeroWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/ChiaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/ChiaWarningsTest.kt new file mode 100644 index 0000000000..3d41a5502f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/ChiaWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/DogecoinWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/DogecoinWarningsTest.kt new file mode 100644 index 0000000000..5156609bb7 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/DogecoinWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KaspaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KaspaWarningsTest.kt new file mode 100644 index 0000000000..98b623c535 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KaspaWarningsTest.kt @@ -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 + ) + } + } + } +} diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt new file mode 100644 index 0000000000..c545a4100b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt new file mode 100644 index 0000000000..be40b64a78 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt new file mode 100644 index 0000000000..ec60a4de62 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt new file mode 100644 index 0000000000..fb4483da57 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/TezosWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/TezosWarningsTest.kt new file mode 100644 index 0000000000..4b65dfa5e8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/TezosWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/XRPWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/XRPWarningsTest.kt new file mode 100644 index 0000000000..bc665f8a2d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/XRPWarningsTest.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 882377e5e5..1603427b46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -310,4 +310,8 @@ + + + + diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 7098dad4f2..59dd05efbe 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 0ef437226d..8db7434ef7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -302,7 +302,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun createAppThemeModeFlow(): SharedFlow { - val tangemApplication = application as TangemApplication + val tangemApplication = requireNotNull(application as? TangemApplication) { + "Application is null" + } return tangemApplication.getAppThemeModeUseCase() .filterNotNull() diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0920d365f9..7804a24b4f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt deleted file mode 100644 index d1aecc29d0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt deleted file mode 100644 index 27f55538da..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt new file mode 100644 index 0000000000..41a70887ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt index 3e687ae5fd..477bf9f930 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt @@ -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, ), ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt index a1931b993e..83e13ae335 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt @@ -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(), ), ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt new file mode 100644 index 0000000000..8ed80f5e60 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt @@ -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) { + params[AnalyticsParam.PRODUCT_TYPE] = "Mobile Wallet" + } + + companion object { + fun id(): String = HotWalletContextInterceptor::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt index 25b67c3ee0..b7309a1366 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index e09ba24e78..0e011c8b21 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt deleted file mode 100644 index 0dfce4b8c7..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index 328a1d431e..39b02987d6 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt b/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt index a04f41db40..cde6e78a86 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 6099bad60a..71fcd29497 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index f944a89717..c847cb4a50 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -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(), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt index 3cd171d04a..078691ca7c 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index ce88071554..ec16f63f50 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -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" diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 1ee8ec2a46..1f40496096 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -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() }, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/HapticModule.kt b/app/src/main/java/com/tangem/tap/di/HapticModule.kt index dc04a3ce72..3db66d0e6e 100644 --- a/app/src/main/java/com/tangem/tap/di/HapticModule.kt +++ b/app/src/main/java/com/tangem/tap/di/HapticModule.kt @@ -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()) { diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index d741fc1a41..0487d0fad7 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index 60fea2b718..c290c819a2 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 7c946ee340..90f6777934 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -3,8 +3,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.usecase.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -50,10 +53,12 @@ internal object AccountDomainModule { fun provideRecoverCryptoPortfolioUseCase( accountsCRUDRepository: AccountsCRUDRepository, mainAccountTokensMigration: MainAccountTokensMigration, + cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, ): RecoverCryptoPortfolioUseCase { return RecoverCryptoPortfolioUseCase( crudRepository = accountsCRUDRepository, mainAccountTokensMigration = mainAccountTokensMigration, + cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, ) } @@ -82,4 +87,16 @@ internal object AccountDomainModule { accountsFeatureToggles = accountsFeatureToggles, ) } + + @Provides + @Singleton + fun provideApplyAccountListSortingUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + dispatchers: CoroutineDispatcherProvider, + ): ApplyAccountListSortingUseCase { + return ApplyAccountListSortingUseCase( + accountsCRUDRepository = accountsCRUDRepository, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index f814746b0a..8c0d2628db 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -33,7 +33,7 @@ internal object CardDomainModule { @Provides @Singleton - fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) + fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig) @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt new file mode 100644 index 0000000000..57fd8da9ae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index d16fc22bd8..ab440ee279 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,5 +1,7 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository @@ -24,9 +26,13 @@ internal object NFTDomainModule { fun providesGetNFTCollectionsUseCase( currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + accountsFeatureToggles: AccountsFeatureToggles, ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( currenciesRepository = currenciesRepository, nftRepository = nftRepository, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, + accountsFeatureToggles = accountsFeatureToggles, ) @Provides @@ -60,10 +66,12 @@ internal object NFTDomainModule { @Singleton fun providesGetNFTAvailableNetworksUseCase( nftRepository: NFTRepository, + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, currenciesRepository: CurrenciesRepository, ): GetNFTNetworksUseCase = GetNFTNetworksUseCase( currenciesRepository = currenciesRepository, nftRepository = nftRepository, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, ) @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0f50feba84..0c20fd0993 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 9a22983e55..c9c0d08868 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 887376f2a3..1fe86d7c5d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -28,7 +28,7 @@ internal object TransactionDomainModule { fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { return GetFeeUseCase( walletManagersFacade = walletManagersFacade, - demoConfig = DemoConfig(), + demoConfig = DemoConfig, ) } @@ -48,7 +48,7 @@ internal object TransactionDomainModule { tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): SendTransactionUseCase { return SendTransactionUseCase( - demoConfig = DemoConfig(), + demoConfig = DemoConfig, cardSdkConfigRepository = cardSdkConfigRepository, transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, @@ -126,7 +126,7 @@ internal object TransactionDomainModule { fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { return EstimateFeeUseCase( walletManagersFacade = walletManagersFacade, - demoConfig = DemoConfig(), + demoConfig = DemoConfig, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index f8af3f022b..16456eb969 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -483,4 +483,12 @@ internal object WalletsDomainModule { dispatchers = dispatcherProvider, ) } + + @Provides + @Singleton + fun provideHasSecuredWalletsUseCase( + userWalletsListRepository: UserWalletsListRepository, + ): HasSecuredWalletsUseCase { + return HasSecuredWalletsUseCase(userWalletsListRepository = userWalletsListRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 9deccc04e6..72762cb7a0 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 17ce65a238..ac18c8b943 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index f632d90194..a3693ba6d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -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() { 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() diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 09a9039974..89c571cd1e 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -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 { + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), + cardId = cardId, + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + } + // endregion companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 1568e7dfc8..d86ebd543b 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -213,5 +213,11 @@ class MockTangemSdkManager( error("Not implemented") } + override suspend fun tangemPayProduceInitialCredentials( + cardId: String, + ): CompletionResult { + error("Not implemented") + } + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 23afd7ef06..3d66410898 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -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, + ), ), ), ), diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index ac172d1b9c..5a3a30ef87 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -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 { 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) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index c1475265db..eb99c14c88 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -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 { - return DemoHelper.config.demoBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet { + return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) } // TODO: Move to user wallet config diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index dd90db55fa..bb0fafa039 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt new file mode 100644 index 0000000000..000ae515ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -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 { + + override fun run(session: CardSession, callback: CompletionCallback) { + coroutineScope.launch { + callback(runSuspend(session = session)) + } + } + + private suspend fun runSuspend(session: CardSession): CompletionResult { + 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 -> 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 -> 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 { + val deferred = CompletableDeferred>() + 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 { + val deferred = CompletableDeferred>() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 547876b22f..27f82f9b3b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 64fc98c01d..3439d72093 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -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, ) { - 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)) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index ae13a2ec94..85b958f1d1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -36,11 +36,6 @@ internal class BiometricUserWalletsListManager( .mapLatest { it.userWallets } .distinctUntilChanged() - override val savedWalletsCount: Flow - get() = state - .mapLatest { walletsCount } - .distinctUntilChanged() - override val userWalletsSync: List get() = state.value.userWallets @@ -71,6 +66,11 @@ internal class BiometricUserWalletsListManager( override val walletsCount: Int get() = state.value.userWallets.size + override val savedWalletsCount: Flow + get() = state + .mapLatest { walletsCount } + .distinctUntilChanged() + override suspend fun unlock(type: UnlockType): CompletionResult { return unlockAndSetSelectedUserWallet(type) .mapFailure { error -> diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index 31ad6301f0..8ee8847ffa 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -20,11 +20,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { .mapLatest { listOfNotNull(it.userWallet) } .distinctUntilChanged() - override val savedWalletsCount: Flow - get() = state - .mapLatest { walletsCount } - .distinctUntilChanged() - override val selectedUserWallet: Flow 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 + get() = state + .mapLatest { walletsCount } + .distinctUntilChanged() + override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { state.value.userWallet ?.takeIf { it.walletId == userWalletId } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 36b368b111..4f38f8e6ce 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -27,6 +27,7 @@ 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.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -149,7 +150,7 @@ internal class DefaultUserWalletsListRepository( val encryptionKey = userWallet.encryptionKey ?: raise(SetLockError.UserWalletLocked) - runCatching { + runSuspendCatching { userWalletEncryptionKeysRepository.save( encryptionKey = UserWalletEncryptionKey( walletId = userWalletId, @@ -232,7 +233,7 @@ internal class DefaultUserWalletsListRepository( val encryptionKey = requestPasswordRecursive( hotWalletId = userWallet.hotWalletId, block = { password -> - runCatching { + runSuspendCatching { userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) }.onFailure { raise(UnlockWalletError.UnableToUnlock) @@ -288,8 +289,7 @@ internal class DefaultUserWalletsListRepository( override suspend fun unlockAllWallets(): Either = either { val userWallets = userWalletsSync() - val userWalletIds = userWallets.map { it.walletId }.toSet() - val biometricKeys = runCatching { + val biometricKeys = runSuspendCatching { userWalletEncryptionKeysRepository.getAllBiometric() }.getOrElse { // TODO handle error properly [REDACTED_TASK_KEY] @@ -309,8 +309,9 @@ 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) } @@ -345,6 +346,12 @@ 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 requestPasswordRecursive( hotWalletId: HotWalletId, block: suspend (CharArray) -> UserWalletEncryptionKey?, diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index e4070d4b60..3f8d2e44cb 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 25964c3070..3a4901f8e3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 = { _, 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) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 5b219a6c22..54a13d6915 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index f7e7eaa6dc..518cd67afb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index d9d1ad29f8..2df9cb3974 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -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)) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 4afa4ca1bb..8a3d691e60 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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 @@ -41,9 +38,7 @@ 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 @@ -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() { @@ -181,7 +177,7 @@ internal class MainViewModel @Inject constructor( .mapLeft { emptyFlow() } .onRight { wallet -> wallet.distinctUntilChanged() - .onEach { Analytics.setContext(it) } + .onEach { trackingContextProxy.setContext(it) } .flowOn(dispatchers.io) .launchIn(viewModelScope) } @@ -195,22 +191,12 @@ internal class MainViewModel @Inject constructor( private fun initializeOffRamp() { viewModelScope.launch { - val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync()) appStateHolder.sellService = sellService sellService.update() } } - private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService { - return MoonPayService( - apiKey = environmentConfig.moonPayApiKey, - secretKey = environmentConfig.moonPayApiSecretKey, - isLogEnabled = LogConfig.network.moonPayService, - userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() }, - ) - } - private fun observeFlips() { listenToFlipsUseCase().launchIn(viewModelScope) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt index 2433af1e95..e0e8bd6b12 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt @@ -14,6 +14,7 @@ import com.tangem.tap.store import kotlinx.coroutines.launch import org.rekotlin.Middleware +@Suppress("MemberNameEqualsClassName") class BackupMiddleware { val backupMiddleware: Middleware = { dispatch, state -> { next -> diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index f6e882e5fb..9ca6d43223 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -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) + } }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 6797731a84..f912f0948a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -100,7 +100,10 @@ internal class WelcomeMiddleware { val currency = ParamCardCurrencyConverter().convert( value = scanResponse.cardTypesResolver, ) - Analytics.addContext(scanResponse) + + val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy) + trackingContextProxy.addContext(scanResponse) + if (currency != null) { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt new file mode 100644 index 0000000000..d26fdea229 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.network.auth + +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.lib.auth.P2PEthPoolAuthProvider + +internal class DefaultP2PEthPoolAuthProvider( + private val environmentConfigStorage: EnvironmentConfigStorage, +) : P2PEthPoolAuthProvider { + + override fun getApiKey(): String { + // val keys = environmentConfigStorage.getConfigSync().p2pApiKey + // ?: error("No P2P api keys provided") + // + // return keys.mainnet + + environmentConfigStorage + + return "TODO restore p2p config call after release 5.30" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index f1ba2de154..b6f4c2bec8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -6,10 +6,12 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider +import com.tangem.tap.network.auth.DefaultP2PEthPoolAuthProvider import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider import com.tangem.utils.version.AppVersionProvider import dagger.Module @@ -50,6 +52,12 @@ internal class AuthModule { return DefaultStakeKitAuthProvider(environmentConfigStorage) } + @Provides + @Singleton + fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider { + return DefaultP2PEthPoolAuthProvider(environmentConfigStorage) + } + @Provides @Singleton fun provideAppVersionProvider(): AppVersionProvider { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index 97cb5f65e6..36f5763779 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -2,44 +2,14 @@ package com.tangem.tap.network.exchangeServices import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import com.tangem.utils.converter.TwoWayConverter +import com.tangem.utils.converter.Converter -internal class CryptoCurrencyConverter( - private val excludedBlockchains: ExcludedBlockchains, -) : TwoWayConverter { +internal object CryptoCurrencyConverter : Converter { - private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } - - override fun convert(value: Currency): CryptoCurrency { - return when (value) { - is Currency.Blockchain -> requireNotNull( - cryptoCurrencyFactory.createCoin( - blockchain = value.blockchain, - extraDerivationPath = value.derivationPath, - userWallet = getSelectedWallet(), - ), - ) - is Currency.Token -> requireNotNull( - cryptoCurrencyFactory.createToken( - sdkToken = value.token, - blockchain = value.blockchain, - extraDerivationPath = value.derivationPath, - userWallet = getSelectedWallet(), - ), - ) - } - } - - override fun convertBack(value: CryptoCurrency): Currency { + override fun convert(value: CryptoCurrency): Currency { val blockchain = value.network.toBlockchain() if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") return when (value) { @@ -60,15 +30,4 @@ internal class CryptoCurrencyConverter( ) } } - - fun getSelectedWallet(): UserWallet { - val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) - val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - requireNotNull(userWalletsListRepository.selectedUserWallet.value) - } else { - requireNotNull(userWalletListManager.selectedUserWalletSync) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 6a0cca84f4..bd69d9835d 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -5,13 +5,11 @@ import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.right -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.api.express.models.response.Asset -import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.domain.core.lce.Lce import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.express.ExpressServiceFetcher +import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -22,26 +20,23 @@ import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull -@Suppress("LongParameterList") internal class DefaultRampManager( - private val sellService: Provider, - private val expressServiceLoader: ExpressServiceLoader, + private val sellService: Provider, + private val expressServiceFetcher: ExpressServiceFetcher, private val currenciesRepository: CurrenciesRepository, private val dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, ) : RampStateManager { - private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains) - override suspend fun availableForBuy( userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): ScenarioUnavailabilityReason { - val availabilityState = runCatching { getOnrampAvailableState(userWallet.walletId, cryptoCurrency) } + val availabilityState = runSuspendCatching { getOnrampAvailableState(userWallet.walletId, cryptoCurrency) } .getOrNull() ?: ExpressAvailabilityState.Error @@ -56,7 +51,7 @@ internal class DefaultRampManager( return either { val isSellSupportedByService = catch( block = { - val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency) + val serviceCurrency = CryptoCurrencyConverter.convert(status.currency) sellService().availableForSell(currency = serviceCurrency) }, @@ -90,13 +85,13 @@ internal class DefaultRampManager( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): ScenarioUnavailabilityReason { - val availabilityState = runCatching { + val availabilityState = runSuspendCatching { getExchangeableState(userWalletId, cryptoCurrency) }.getOrNull() ?: ExpressAvailabilityState.Error return availabilityState.toReason(cryptoCurrency.name) } - override fun getSellInitializationStatus(): Flow { + override fun getSellInitializationStatus(): Flow { return sellService.invoke().initializationStatus } @@ -106,8 +101,8 @@ internal class DefaultRampManager( } } - override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow { - return expressServiceLoader.getInitializationStatus(userWalletId) + override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow { + return expressServiceFetcher.getInitializationStatus(userWalletId) } override suspend fun getSendUnavailabilityReason( @@ -151,14 +146,14 @@ internal class DefaultRampManager( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): ExpressAvailabilityState { - val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() + val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull() ?: return ExpressAvailabilityState.Loading return when (asset) { is Lce.Error -> ExpressAvailabilityState.Error is Lce.Loading -> ExpressAvailabilityState.Loading is Lce.Content -> { - val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } - foundAsset?.exchangeAvailable?.toSwapAvailabilityState() + val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) } + foundAsset?.isExchangeAvailable?.toSwapAvailabilityState() ?: ExpressAvailabilityState.AssetNotFound } } @@ -168,15 +163,15 @@ internal class DefaultRampManager( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): ExpressAvailabilityState { - val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() + val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull() ?: return ExpressAvailabilityState.Loading return when (asset) { is Lce.Error -> ExpressAvailabilityState.Error is Lce.Loading -> ExpressAvailabilityState.Loading is Lce.Content -> { - val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } - foundAsset?.onrampAvailable?.toOnrampAvailabilityState() + val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) } + foundAsset?.isOnrampAvailable?.toOnrampAvailabilityState() ?: ExpressAvailabilityState.AssetNotFound } } @@ -211,8 +206,13 @@ internal class DefaultRampManager( } } - private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean { - val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true) + private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean { + val currencyAssedId = ExpressAsset.ID( + networkId = this.network.backendId, + contractAddress = (this as? CryptoCurrency.Token)?.contractAddress, + ) + + return assetId.networkId == currencyAssedId.networkId && + assetId.contractAddress.equals(currencyAssedId.contractAddress, ignoreCase = true) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt similarity index 75% rename from app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt rename to app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt index 88ba965edc..82aa59fe63 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt @@ -5,11 +5,11 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.tap.domain.model.Currency import kotlinx.coroutines.flow.StateFlow -typealias ExchangeServiceInitializationStatus = Lce +typealias SellServiceInitializationStatus = Lce -interface ExchangeService { +interface SellService { - val initializationStatus: StateFlow + val initializationStatus: StateFlow suspend fun update() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 1f291b109b..60491ab4c7 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -5,7 +5,9 @@ import android.util.Base64 import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.datasource.api.common.createRetrofitInstance +import com.tangem.datasource.api.moonpay.MoonPayApi +import com.tangem.datasource.api.moonpay.MoonPayCurrencies +import com.tangem.datasource.api.moonpay.MoonPayUserStatus import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.core.utils.lceContent @@ -14,9 +16,10 @@ import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.domain.model.Currency -import com.tangem.tap.network.exchangeServices.ExchangeService -import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus +import com.tangem.tap.network.exchangeServices.SellService +import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency +import com.tangem.utils.Provider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import timber.log.Timber @@ -24,24 +27,17 @@ import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec class MoonPayService( - private val apiKey: String, - private val secretKey: String, - private val isLogEnabled: Boolean, + private val api: MoonPayApi, + private val apiKeyProvider: Provider, + private val secretKeyProvider: Provider, private val userWalletProvider: () -> UserWallet?, -) : ExchangeService { +) : SellService { - override val initializationStatus: StateFlow - get() = _initializationStatus - - private val _initializationStatus: MutableStateFlow = + private val _initializationStatus: MutableStateFlow = MutableStateFlow(value = lceLoading()) - private val api: MoonPayApi by lazy { - createRetrofitInstance( - baseUrl = MoonPayApi.MOOONPAY_BASE_URL, - logEnabled = isLogEnabled, - ).create(MoonPayApi::class.java) - } + override val initializationStatus: StateFlow + get() = _initializationStatus private var status: MoonPayStatus? = null @@ -51,7 +47,7 @@ class MoonPayService( _initializationStatus.value = lceLoading() performRequest { - val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { + val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) { is Result.Failure -> { Timber.e("Failed to load user status", result.error) _initializationStatus.value = result.error.lceError() @@ -60,7 +56,7 @@ class MoonPayService( is Result.Success -> result.data } - val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { + val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) { is Result.Failure -> { Timber.e("Failed to load currencies", result.error) _initializationStatus.value = result.error.lceError() @@ -78,7 +74,7 @@ class MoonPayService( MoonPayAvailableCurrency( currencyCode = currency.code, networkCode = currency.metadata?.networkCode ?: return@mapNotNull null, - contractAddress = currency.metadata.contractAddress, + contractAddress = currency.metadata?.contractAddress, ) } @@ -167,7 +163,7 @@ class MoonPayService( val uri = Uri.Builder() .scheme(SCHEME) .authority(URL_SELL) - .appendQueryParameter("apiKey", apiKey) + .appendQueryParameter("apiKey", apiKeyProvider()) .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}") @@ -191,7 +187,7 @@ class MoonPayService( private fun createSignature(data: String): String { val sha256Hmac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") + val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256") sha256Hmac.init(secretKey) val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray()) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index be7e116dd6..642cdea9ac 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -160,6 +160,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Pepecoin, PepecoinTestnet -> null Hyperliquid, HyperliquidTestnet -> null Quai, QuaiTestnet -> null - // Linea, LineaTestnet -> null - // ArbitrumNova -> null + Linea, LineaTestnet -> null + ArbitrumNova -> null } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cee10eaf8f..c3261b1873 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -7,7 +7,7 @@ import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.SellService import org.rekotlin.Action import org.rekotlin.Store import javax.inject.Inject @@ -19,7 +19,7 @@ import javax.inject.Inject class AppStateHolder @Inject constructor() : ReduxStateHolder { var mainStore: Store? = null - var sellService: ExchangeService? = null + var sellService: SellService? = null override fun dispatch(action: Action) { mainStore?.dispatch(action) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt index ee884e4e2a..0e822a4edc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux import com.tangem.tap.common.redux.AppState import org.rekotlin.Middleware +@Suppress("MemberNameEqualsClassName") object DaggerGraphMiddleware { val daggerGraphMiddleware: Middleware = { _, _ -> { next -> diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 0b32197bce..8095c3df87 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager @@ -79,4 +80,5 @@ data class DaggerGraphState( val userWalletsListRepository: UserWalletsListRepository? = null, val hotWalletFeatureToggles: HotWalletFeatureToggles? = null, val tangemHotSdk: TangemHotSdk? = null, + val trackingContextProxy: TrackingContextProxy? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index 492ca42a8b..ad66a52dce 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -40,6 +40,12 @@ internal class ProxyAppRouter( } } + override fun replaceCurrent(route: AppRoute, onComplete: (Boolean) -> Unit) { + safeNavigate(onComplete, message = "Replace a current route with $route") { + innerRouter.replaceCurrent(route, onComplete) + } + } + override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { safeNavigate(onComplete, message = "Replace all routes with $routes") { runCatching { diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index 66b66199d4..b95777a48b 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -6,6 +6,7 @@ import android.os.Bundle import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -83,6 +84,7 @@ internal fun RootContent( TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) + .imePadding() .navigationBarsPadding() .padding(all = 16.dp), hostState = snackbarHostState, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 2059299bb3..791434d17b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -36,9 +36,10 @@ import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent -import com.tangem.features.tangempay.components.TangemPayDetailsComponent +import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent -import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -62,6 +63,7 @@ internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val walletBackupComponentFactory: WalletBackupComponent.Factory, + private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, @@ -100,6 +102,7 @@ internal class ChildFactory @Inject constructor( private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory, + private val createHardwareWalletComponentFactory: CreateHardwareWalletComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, @@ -107,8 +110,9 @@ internal class ChildFactory @Inject constructor( private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val viewPhraseComponentFactory: ViewPhraseComponent.Factory, + private val forgetWalletComponentFactory: ForgetWalletComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, - private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, + private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory, @@ -138,7 +142,6 @@ internal class ChildFactory @Inject constructor( is AppRoute.ManageTokens -> { val source = when (route.source) { AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS - AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES } @@ -187,6 +190,15 @@ internal class ChildFactory @Inject constructor( componentFactory = walletBackupComponentFactory, ) } + is AppRoute.WalletHardwareBackup -> { + createComponentChild( + context = context, + params = WalletHardwareBackupComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = walletHardwareBackupComponentFactory, + ) + } is AppRoute.MarketsTokenDetails -> { createComponentChild( context = context, @@ -294,7 +306,7 @@ internal class ChildFactory @Inject constructor( context = context, params = StakingComponent.Params( userWalletId = route.userWalletId, - cryptoCurrencyId = route.cryptoCurrencyId, + cryptoCurrency = route.cryptoCurrency, yieldId = route.yieldId, ), componentFactory = stakingComponentFactory, @@ -309,6 +321,13 @@ internal class ChildFactory @Inject constructor( userWalletId = route.userWalletId, isInitialReverseOrder = route.isInitialReverseOrder, screenSource = route.screenSource, + tangemPayInput = route.tangemPayInput?.let { tangemPayInput -> + SwapComponent.Params.TangemPayInput( + cryptoAmount = tangemPayInput.cryptoAmount, + fiatAmount = tangemPayInput.fiatAmount, + depositAddress = tangemPayInput.depositAddress, + ) + }, ), componentFactory = swapComponentFactory, ) @@ -497,6 +516,13 @@ internal class ChildFactory @Inject constructor( componentFactory = createWalletSelectionComponentFactory, ) } + is AppRoute.CreateHardwareWallet -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = createHardwareWalletComponentFactory, + ) + } is AppRoute.CreateMobileWallet -> { createComponentChild( context = context, @@ -525,6 +551,7 @@ internal class ChildFactory @Inject constructor( context = context, params = WalletActivationComponent.Params( userWalletId = route.userWalletId, + isBackupExists = route.isBackupExists, ), componentFactory = walletActivationComponentFactory, ) @@ -534,6 +561,8 @@ internal class ChildFactory @Inject constructor( context = context, params = CreateWalletBackupComponent.Params( userWalletId = route.userWalletId, + isUpgradeFlow = route.isUpgradeFlow, + setAccessCode = route.setAccessCode, ), componentFactory = createWalletBackupComponentFactory, ) @@ -543,6 +572,7 @@ internal class ChildFactory @Inject constructor( context = context, params = UpdateAccessCodeComponent.Params( userWalletId = route.userWalletId, + isFirstSetup = route.isFirstSetup, ), componentFactory = updateAccessCodeComponentFactory, ) @@ -556,6 +586,15 @@ internal class ChildFactory @Inject constructor( componentFactory = viewPhraseComponentFactory, ) } + is AppRoute.ForgetWallet -> { + createComponentChild( + context = context, + params = ForgetWalletComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = forgetWalletComponentFactory, + ) + } is AppRoute.SendEntryPoint -> { createComponentChild( context = context, @@ -605,8 +644,11 @@ internal class ChildFactory @Inject constructor( is AppRoute.TangemPayDetails -> { createComponentChild( context = context, - params = TangemPayDetailsComponent.Params(config = route.config), - componentFactory = tangemPayDetailsComponentFactory, + params = TangemPayDetailsContainerComponent.Params( + userWalletId = route.userWalletId, + config = route.config, + ), + componentFactory = tangemPayDetailsContainerComponentFactory, ) } is AppRoute.TangemPayOnboarding -> { diff --git a/build.gradle.kts b/build.gradle.kts index 3648de0c58..b59181ab1b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,18 +34,36 @@ interface Injected { // Test Logging subprojects { - tasks.withType { + tasks.withType().configureEach { + val taskName = name.lowercase() + if (taskName.contains("external") || + taskName.contains("internal") || + taskName.contains("release") || + taskName.contains("mocked") || + taskName.contains("huawei") + ) { + enabled = false + println("Skipping test task: $name") + } else { + println("Test task scheduled: $name") + } + testLogging { exceptionFormat = TestExceptionFormat.FULL showStandardStreams = true afterSuite(KotlinClosure2({ desc, result -> if (desc.parent == null) { // will match the outermost suite - val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + val output = + "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" val startItem = "| " val endItem = " |" val repeatLength = startItem.length + output.length + endItem.length - println("\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(repeatLength)) + println( + "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( + repeatLength + ) + ) } })) } diff --git a/common/routing/detekt-baseline-debug.xml b/common/routing/detekt-baseline-debug.xml new file mode 100644 index 0000000000..84f08bf7e3 --- /dev/null +++ b/common/routing/detekt-baseline-debug.xml @@ -0,0 +1,9 @@ + + + + + BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false + NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) } + NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) } + + diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d9f6179153..95aab2d822 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -15,6 +15,7 @@ import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.onramp.model.OnrampSource @@ -128,9 +129,12 @@ sealed class AppRoute(val path: String) : Route { val portfolioId: PortfolioId? = null, ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { + /** + * Source of launching the screen. + * ManageTokens screen launched from Onboarding by another route. See `OnboardingRoute.ManageTokens`. + */ enum class Source { STORIES, - ONBOARDING, SETTINGS, } } @@ -180,13 +184,21 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val isInitialReverseOrder: Boolean = false, val screenSource: String, + val tangemPayInput: TangemPayInput? = null, ) : AppRoute( path = "/swap" + "/${currencyFrom.id.value}" + "/${currencyTo?.id?.value}" + "/${userWalletId.stringValue}" + "/$isInitialReverseOrder", - ) + ) { + @Serializable + data class TangemPayInput( + val cryptoAmount: SerializedBigDecimal, + val fiatAmount: SerializedBigDecimal, + val depositAddress: String, + ) + } @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") @@ -194,9 +206,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Staking( val userWalletId: UserWalletId, - val cryptoCurrencyId: CryptoCurrency.ID, + val cryptoCurrency: CryptoCurrency, val yieldId: String, - ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId") + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId") @Serializable data class PushNotification( @@ -219,6 +231,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}") + @Serializable + data class WalletHardwareBackup( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}") + @Serializable data object Markets : AppRoute(path = "/markets") @@ -324,6 +341,9 @@ sealed class AppRoute(val path: String) : Route { } } + @Serializable + object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet") + @Serializable object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") @@ -338,16 +358,20 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class WalletActivation( val userWalletId: UserWalletId, + val isBackupExists: Boolean, ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") @Serializable data class CreateWalletBackup( val userWalletId: UserWalletId, + val isUpgradeFlow: Boolean = false, + val setAccessCode: Boolean = false, ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") @Serializable data class UpdateAccessCode( val userWalletId: UserWalletId, + val isFirstSetup: Boolean, ) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}") @Serializable @@ -355,6 +379,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}") + @Serializable + data class ForgetWallet( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/forget_wallet/${userWalletId.stringValue}") + @Serializable data class SendEntryPoint( val userWalletId: UserWalletId, @@ -385,8 +414,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class TangemPayDetails( + val userWalletId: UserWalletId, val config: TangemPayDetailsConfig, - ) : AppRoute(path = "/tangem_pay_details") + ) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}") @Serializable data class TangemPayOnboarding( diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt index 9f8d5f4111..1f03d452d1 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -30,6 +30,19 @@ interface AppRouter { }, ) + /** + * Replaces a current route with a new one. + * + * @param route The route to replace a current one. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceCurrent( + route: AppRoute, + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to replace a current route with $route") + }, + ) + /** * Replaces ***all*** routes in the navigation stack with the specified [routes]. * diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt index 0761de97a5..cacdcfadba 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt @@ -28,6 +28,12 @@ private class RouterProxy( } } + override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) { + if (route is AppRoute) { + appRouter.replaceCurrent(route, onComplete) + } + } + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { routes.filterIsInstance().let { appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete) diff --git a/common/test/detekt-baseline-debug.xml b/common/test/detekt-baseline-debug.xml new file mode 100644 index 0000000000..27ff65dbe7 --- /dev/null +++ b/common/test/detekt-baseline-debug.xml @@ -0,0 +1,8 @@ + + + + + CastNullableToNonNullableType:MockScanResponseFactory.kt$MockScanResponseFactory$as + MultilineLambdaItParameter:MockScanResponseFactory.kt$MockScanResponseFactory${ CardDTO.Wallet( CardWallet( publicKey = it.name.toByteArray(), // IMPORTANT: public key must equal to curve name chainCode = null, curve = it, settings = createSettings(), totalSignedHashes = null, remainingSignatures = null, index = 0, isImported = true, hasBackup = true, derivedKeys = emptyMap(), ), ) } + + diff --git a/common/ui-charts/detekt-baseline-debug.xml b/common/ui-charts/detekt-baseline-debug.xml new file mode 100644 index 0000000000..05694da92a --- /dev/null +++ b/common/ui-charts/detekt-baseline-debug.xml @@ -0,0 +1,7 @@ + + + + + SuspendFunSwallowedCancellation:MarketChartDataProducer.kt$MarketChartDataProducer$runCatching + + diff --git a/common/ui/detekt-baseline-debug.xml b/common/ui/detekt-baseline-debug.xml new file mode 100644 index 0000000000..76624db4ad --- /dev/null +++ b/common/ui/detekt-baseline-debug.xml @@ -0,0 +1,32 @@ + + + + + BooleanPropertyNaming:AddTokenUM.kt$AddTokenUM.Button$val showProgress: Boolean + BooleanPropertyNaming:AddTokenUM.kt$AddTokenUM.Network$val editable: Boolean + BooleanPropertyNaming:AmountCurrencyTransformer.kt$AmountCurrencyTransformer$private val value: Boolean + BooleanPropertyNaming:GiveTxPermissionState.kt$CancelPermissionButton$val enabled: Boolean + BooleanPropertyNaming:NotificationUM.kt$NotificationUM.Error.ExceedsBalance$val mergeFeeNetworkName: Boolean = false + BooleanPropertyNaming:NotificationsFactory.kt$NotificationsFactory$val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero() + BooleanPropertyNaming:TokenReceiveBottomSheetConfig.kt$TokenReceiveBottomSheetConfig$val showMemoDisclaimer: Boolean + CanBeNonNullable:AmountBlockV2.kt$onClick: (() -> Unit)? = null + CanBeNonNullable:NavigationButtonsBlock.kt$footerText: TextReference? + CanBeNonNullable:NavigationButtonsBlock.kt$pairButtons: Pair<NavigationButton, NavigationButton>? + CanBeNonNullable:NavigationButtonsBlock.kt$prevButton: NavigationButton? + CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$dustValue: BigDecimal? + CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minAdaValue: BigDecimal? + CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal? + CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent? + MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -> { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -> { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -> null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) } + MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter${ createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) } + MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) } + NamedArguments:TokenItemStateConverter.kt$TokenItemStateConverter$createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) + NoNameShadowing:NavigationButtonsBlock.kt$navigationUM + NoNameShadowing:UserWalletItem.kt$balance + NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false + NullableToStringCall:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$${id.rawCurrencyId} + ReusedModifierInstance:AddTokenContent.kt$AddButton( modifier = modifier.fillMaxWidth(), state = state.button, ) + UnnecessaryEventHandlerParameter:SendDoneButtons.kt$onShareClick: (String) -> Unit + UnnecessaryLet:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$let(::add) + + diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt index e2b2b60cd9..c24dd49e89 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt @@ -1,21 +1,31 @@ package com.tangem.common.ui.account import com.tangem.common.ui.R +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState +import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.quote.PriceChange import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero +import java.math.BigDecimal class AccountCryptoPortfolioItemStateConverter( private val appCurrency: AppCurrency, private val account: Account.CryptoPortfolio, + private val priceChangeLce: Lce? = null, private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null, private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null, ) : Converter { @@ -31,8 +41,16 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToContentState( fiatBalance: TotalFiatBalance.Loaded, ): TokenItemState.Content { + val subtitle2State = when (fiatBalance.amount.isZero()) { + true -> null + false -> priceChangeLce?.fold( + ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading }, + ifError = { null }, + ifContent = { priceChange -> priceChange.toSubtitle2State() }, + ) + } return TokenItemState.Content( - id = account.accountId.value, + id = account.accountId.toItemId(), iconState = AccountIconItemStateConverter.convert(this), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, @@ -50,15 +68,15 @@ class AccountCryptoPortfolioItemStateConverter( .format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, isFlickering = fiatBalance.source == StatusSource.CACHE, ), - subtitle2State = null, + subtitle2State = subtitle2State, onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } }, onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } }, ) } - private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Loading { - return TokenItemState.Loading( - id = account.accountId.value, + private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content { + return TokenItemState.Content( + id = account.accountId.toItemId(), iconState = AccountIconItemStateConverter.convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, @@ -71,12 +89,16 @@ class AccountCryptoPortfolioItemStateConverter( ), isAvailable = false, ), + fiatAmountState = FiatAmountState.Loading, + subtitle2State = Subtitle2State.Loading, + onItemLongClick = null, + onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } }, ) } private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( - id = account.accountId.value, + id = account.accountId.toItemId(), iconState = AccountIconItemStateConverter.convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, @@ -97,4 +119,16 @@ class AccountCryptoPortfolioItemStateConverter( }, ) } + + private fun AccountId.toItemId() = this.userWalletId.stringValue + this.value + + private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this) + + private fun StatusSource.isFlickering(): Boolean = this == StatusSource.CACHE + + private fun PriceChange.toSubtitle2State(): Subtitle2State = Subtitle2State.PriceChangeContent( + priceChangePercent = this.value.format { percent() }, + type = this.value.getPriceChangeType(), + isFlickering = this.source.isFlickering(), + ) } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountNameUM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountNameUM.kt index e86dda636b..305212c2bc 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountNameUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountNameUM.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.account +import androidx.annotation.StringRes import androidx.compose.runtime.Immutable import arrow.core.Either import arrow.core.raise.either @@ -25,7 +26,10 @@ sealed interface AccountNameUM { */ data object DefaultMain : AccountNameUM { - override val value: TextReference = resourceReference(R.string.account_main_account_title) + @get:StringRes + val stringResId: Int = R.string.account_main_account_title + + override val value: TextReference = resourceReference(stringResId) } /** @@ -33,7 +37,7 @@ sealed interface AccountNameUM { * * @property raw the raw string value of the custom account name */ - class Custom(internal val raw: String) : AccountNameUM { + data class Custom(internal val raw: String) : AccountNameUM { override val value: TextReference = stringReference(value = raw) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt index e270e01789..d45ef488d1 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt @@ -10,12 +10,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.converter.Converter class AccountPortfolioItemUMConverter( - private val onClick: (AccountId) -> Unit, + private val onClick: () -> Unit, private val appCurrency: AppCurrency? = null, private val accountBalance: TotalFiatBalance? = null, private val isBalanceHidden: Boolean = false, @@ -26,14 +24,14 @@ class AccountPortfolioItemUMConverter( override fun convert(value: Account): UserWalletItemUM { return with(value) { UserWalletItemUM( - id = UserWalletId(value.accountId.value), - name = value.accountName.toUM().value, - information = getInfo(value), + id = accountId.value, + name = accountName.toUM().value, + information = getInfo(account = this), balance = getBalanceInfo(), isEnabled = isEnabled, endIcon = endIcon, - onClick = { onClick(value.accountId) }, - imageState = getImageState(value), + onClick = onClick, + imageState = getImageState(account = this), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt index e2d1c5b4c1..d21a35276b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp @@ -29,6 +30,8 @@ fun AccountTitle( accountTitleUM: AccountTitleUM, modifier: Modifier = Modifier, textStyle: TextStyle = TangemTheme.typography.subtitle2, + textColor: Color = TangemTheme.colors.text.tertiary, + iconSize: AccountIconSize = AccountIconSize.ExtraSmall, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -40,19 +43,20 @@ fun AccountTitle( Text( text = accountTitleUM.prefixText.resolveReference(), style = textStyle, - color = TangemTheme.colors.text.tertiary, + color = textColor, ) AccountLabel( name = accountTitleUM.name, icon = accountTitleUM.icon, - iconSize = AccountIconSize.ExtraSmall, + iconSize = iconSize, nameStyle = textStyle, + nameColor = textColor, ) } is AccountTitleUM.Text -> Text( text = accountTitleUM.title.resolveReference(), style = textStyle, - color = TangemTheme.colors.text.tertiary, + color = textColor, modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE), ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt new file mode 100644 index 0000000000..caa4ec4ce0 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -0,0 +1,127 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.AccountName + +@Composable +fun PortfolioSelectRow( + state: PortfolioSelectUM, + modifier: Modifier = Modifier, + leftContent: @Composable RowScope.() -> Unit = {}, +) { + Row( + modifier = modifier + .clickable(enabled = state.isMultiChoice, onClick = state.onClick) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + leftContent() + val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet + Text( + modifier = Modifier.weight(1f), + text = stringResourceSafe(leftText), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerW12() + if (state.icon != null) { + AccountIcon( + name = state.name, + icon = state.icon, + size = AccountIconSize.Small, + ) + } + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 4.dp), + text = state.name.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + if (state.isMultiChoice) { + Icon( + modifier = Modifier + .size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } +} + +@Immutable +data class PortfolioSelectUM( + val icon: CryptoPortfolioIconUM?, + val name: TextReference, + val isAccountMode: Boolean, + val isMultiChoice: Boolean, + val onClick: () -> Unit, +) + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class) state: PortfolioSelectUM) { + TangemThemePreview { + PortfolioSelectRow( + state = state, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + ) + } +} + +private class PreviewProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf(PortfolioSelectRowPreviewData.account, PortfolioSelectRowPreviewData.wallet) +} + +object PortfolioSelectRowPreviewData { + val account + get() = PortfolioSelectUM( + icon = AccountIconPreviewData.randomAccountIcon(), + name = AccountName.DefaultMain.toUM().value, + isAccountMode = true, + isMultiChoice = true, + onClick = {}, + ) + val wallet + get() = PortfolioSelectUM( + icon = null, + name = stringReference("Wallet Name"), + isMultiChoice = false, + isAccountMode = false, + onClick = {}, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/AddTokenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContent.kt similarity index 75% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/AddTokenContent.kt rename to common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContent.kt index 02464553bd..5e0f916630 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/AddTokenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.portfolio.add.impl.ui +package com.tangem.common.ui.addtoken import android.content.res.Configuration import androidx.compose.foundation.background @@ -9,6 +9,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import com.tangem.core.ui.R import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -22,14 +23,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.common.ui.account.AccountIcon import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.PortfolioSelectRow +import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.common.ui.account.toUM import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.TokenItem @@ -44,12 +45,10 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags import com.tangem.domain.models.account.AccountName -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM import java.util.UUID @Composable -internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) { +fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) { Column( modifier = modifier.fillMaxWidth(), ) { @@ -63,12 +62,11 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) { SpacerH(TangemTheme.dimens.spacing14) Column( - modifier = Modifier.background( - color = TangemTheme.colors.background.action, - shape = RoundedCornerShape(TangemTheme.dimens.radius14), - ), + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action), ) { - PortfolioRow(state.portfolio) + PortfolioSelectRow(state.portfolio) HorizontalDivider( modifier = Modifier.padding(horizontal = 12.dp), thickness = TangemTheme.dimens.size0_5, @@ -86,50 +84,6 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) { } } -@Composable -private fun PortfolioRow(state: AddTokenUM.Portfolio, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clickable(enabled = state.editable, onClick = state.onClick) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet - Text( - modifier = Modifier.weight(1f), - text = stringResourceSafe(leftText), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - SpacerW12() - if (state.accountIconUM != null) { - AccountIcon( - name = state.name, - icon = state.accountIconUM, - size = AccountIconSize.Small, - ) - } - Text( - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 4.dp), - text = state.name.resolveReference(), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - if (state.editable) { - Icon( - modifier = Modifier - .size(width = 18.dp, height = 24.dp) - .testTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - @Composable private fun NetworkRow(state: AddTokenUM.Network, modifier: Modifier = Modifier) { Row( @@ -240,17 +194,19 @@ private class PreviewProvider : PreviewParameterProvider { ) val account - get() = AddTokenUM.Portfolio( - accountIconUM = AccountIconPreviewData.randomAccountIcon(), + get() = PortfolioSelectUM( + icon = AccountIconPreviewData.randomAccountIcon(), name = AccountName.DefaultMain.toUM().value, - editable = true, + isAccountMode = true, + isMultiChoice = true, onClick = {}, ) val wallet - get() = AddTokenUM.Portfolio( - accountIconUM = null, - name = stringReference("Wallet"), - editable = true, + get() = PortfolioSelectUM( + icon = null, + name = stringReference("Wallet Name"), + isMultiChoice = false, + isAccountMode = false, onClick = {}, ) @@ -271,7 +227,7 @@ private class PreviewProvider : PreviewParameterProvider { AddTokenUM( tokenToAdd = tokenState, network = networkUM.copy(editable = false), - portfolio = wallet.copy(editable = false), + portfolio = wallet.copy(isMultiChoice = false), button = button.copy( isEnabled = true, isTangemIconVisible = true, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/AddTokenUM.kt b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenUM.kt similarity index 61% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/AddTokenUM.kt rename to common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenUM.kt index 7ab163ffb2..5457f68372 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/AddTokenUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenUM.kt @@ -1,24 +1,16 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state +package com.tangem.common.ui.addtoken import androidx.annotation.DrawableRes -import com.tangem.common.ui.account.CryptoPortfolioIconUM +import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference data class AddTokenUM( val tokenToAdd: TokenItemState, val network: Network, - val portfolio: Portfolio, + val portfolio: PortfolioSelectUM, val button: Button, ) { - data class Portfolio( - val accountIconUM: CryptoPortfolioIconUM?, - val name: TextReference, - val editable: Boolean, - val onClick: () -> Unit, - ) { - val isAccountMode get() = accountIconUM != null - } data class Network( @DrawableRes val icon: Int, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index 6360babb2d..0932e007ab 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection @@ -47,6 +48,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @@ -191,7 +193,8 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre .size(16.dp) .graphicsLayer { rotationY = iconRotateState - }, + } + .testTag(SendScreenTestTags.EXCHANGE_ICON), ) AmountFieldCurrencyIcon( amountUM = amountUM, @@ -227,6 +230,7 @@ private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) { style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, + modifier = Modifier.testTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT), ) if (isFiatValue) { CurrencyIcon( diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 5f431fee2f..4b35beb47b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -37,10 +37,12 @@ import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.THREE_STARS @@ -322,7 +324,7 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider get() = sequenceOf( UserWalletItemUM( - id = UserWalletId("user_wallet_0".encodeToByteArray()), + id = "user_wallet_0", name = stringReference("Mobile Wallet"), information = getInformation(cardCount = 1), balance = UserWalletItemUM.Balance.Locked, @@ -331,7 +333,7 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider