Updated on 2026-08-14
This commit is contained in:
commit
f2b72b3771
66 changed files with 1955 additions and 275 deletions
|
|
@ -87,4 +87,53 @@ fun BaseTestCase.openGaslessSendScreenWithHotWallet(
|
|||
step("Click on 'Send' button in bottom sheet") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an existing hot wallet, select the token to send and choose the swap target token/network —
|
||||
* the shared entry into the gasless send-via-swap flow. Scenario states stay in the test body.
|
||||
*/
|
||||
fun BaseTestCase.openSendViaSwapScreenWithHotWallet(
|
||||
seedPhrase: String,
|
||||
tokenName: String,
|
||||
swapTokenName: String,
|
||||
networkName: String,
|
||||
networkType: String? = null,
|
||||
) {
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Select '$swapTokenName' as the token to receive via swap") {
|
||||
selectTokenToSendViaSwap(
|
||||
swapTokenName = swapTokenName,
|
||||
networkName = networkName,
|
||||
networkType = networkType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send-via-swap amount entry: type the amount, advance past the quote-gated 'Next' button (waiting
|
||||
* until it becomes enabled once the swap quote loads), then fill the recipient and open the
|
||||
* 'Send confirm' screen. Uses `composeTestRule.waitUntil` because `flakySafely` is unavailable in
|
||||
* extensions on [BaseTestCase].
|
||||
*/
|
||||
fun BaseTestCase.enterSwapAmountAndOpenSendConfirm(amount: String, recipientAddress: String) {
|
||||
step("Type amount '$amount' in input field") {
|
||||
onSendScreen { amountInputTextField.performTextReplacement(amount) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching {
|
||||
onSendScreen {
|
||||
nextButton.assertIsEnabled()
|
||||
nextButton.performClick()
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
}
|
||||
enterRecipientAndOpenSendConfirm(recipientAddress)
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
package com.tangem.tests.send.sendViaSwap
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.R
|
||||
import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.PROVIDERS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
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
|
||||
|
||||
/**
|
||||
* Gasless send-via-swap: paying the network fee with the stablecoin while converting it through an
|
||||
* express swap. Covers the fee-token selection on the swap summary, the stablecoin balance validation
|
||||
* against the gasless fee, and the full signed swap-and-send on a hot wallet.
|
||||
*/
|
||||
@HiltAndroidTest
|
||||
class GaslessSendViaSwapTest : BaseTestCase() {
|
||||
|
||||
private val tokenName = "USDC"
|
||||
private val currencySymbol = "USDC"
|
||||
private val nativeTokenName = "Polygon"
|
||||
private val swapTokenName = "Bitcoin"
|
||||
private val mainNetwork = "MAIN"
|
||||
private val providerName = "Changelly"
|
||||
private val tokenAmount = "1"
|
||||
private val hotWalletTokensState = "PolygonUSDCHotWallet"
|
||||
private val quotesState = "PolygonUSDC"
|
||||
private val assetsScenarioName = "express_api_assets"
|
||||
private val assetsExchangeEnabledState = "BitcoinExchangeEnabled"
|
||||
private val providersState = "HotWalletSvS"
|
||||
|
||||
@AllureId("5120")
|
||||
@DisplayName("Gasless Send via Swap: the network fee is selectable and payable with the stablecoin")
|
||||
@Test
|
||||
fun checkFeeTokenSelectionForSwapTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(assetsScenarioName)
|
||||
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||
}
|
||||
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||
}
|
||||
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||
}
|
||||
|
||||
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||
openSendViaSwapScreenWithHotWallet(
|
||||
seedPhrase = SVS_SEED_PHRASE_12,
|
||||
tokenName = tokenName,
|
||||
swapTokenName = swapTokenName,
|
||||
networkName = swapTokenName,
|
||||
networkType = mainNetwork,
|
||||
)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Assert 'Network fee' block with token selection is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorTitle.assertIsDisplayed()
|
||||
selectFeeIcon.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen { feeSelectorBlock.performClick() }
|
||||
}
|
||||
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||
}
|
||||
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$tokenName' is available for the fee payment") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).assertIsDisplayed() }
|
||||
}
|
||||
step("Select '$tokenName' as the fee-paying token") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
step("Assert the network fee is calculated in '$currencySymbol' on the summary") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5121")
|
||||
@DisplayName("Gasless Send via Swap: insufficient stablecoin balance to cover the fee blocks the swap")
|
||||
@Test
|
||||
fun checkBalanceValidationForFeeTest() {
|
||||
val usdcBalanceScenario = "polygon_usdc_balance"
|
||||
val lowBalanceState = "LowBalance"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(assetsScenarioName)
|
||||
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||
resetWireMockScenarioState(usdcBalanceScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
|
||||
setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
|
||||
}
|
||||
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||
}
|
||||
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||
}
|
||||
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||
}
|
||||
|
||||
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||
openSendViaSwapScreenWithHotWallet(
|
||||
seedPhrase = SVS_SEED_PHRASE_12,
|
||||
tokenName = tokenName,
|
||||
swapTokenName = swapTokenName,
|
||||
networkName = swapTokenName,
|
||||
networkType = mainNetwork,
|
||||
)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5122")
|
||||
@DisplayName("Gasless Send via Swap: sign and send a swap paying the fee with the stablecoin")
|
||||
@Test
|
||||
fun checkSendViaSwapFinalScreenAndSendTest() {
|
||||
val exchangeStatusScenario = "exchange_status_provider"
|
||||
val changellyStatusState = "Changelly"
|
||||
val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(assetsScenarioName)
|
||||
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||
resetWireMockScenarioState(exchangeStatusScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||
}
|
||||
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||
}
|
||||
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||
}
|
||||
step("Set WireMock scenario '$exchangeStatusScenario' to '$changellyStatusState'") {
|
||||
setWireMockScenarioState(scenarioName = exchangeStatusScenario, state = changellyStatusState)
|
||||
}
|
||||
|
||||
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||
openSendViaSwapScreenWithHotWallet(
|
||||
seedPhrase = SVS_SEED_PHRASE_12,
|
||||
tokenName = tokenName,
|
||||
swapTokenName = swapTokenName,
|
||||
networkName = swapTokenName,
|
||||
networkType = mainNetwork,
|
||||
)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
step("Assert the sent '$tokenName' amount is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen { primaryAmount.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert the recipient address is displayed") {
|
||||
onSendConfirmScreen { recipientAddress(BITCOIN_RECIPIENT_ADDRESS).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert the amount to receive after the swap is displayed") {
|
||||
onSendConfirmScreen { secondaryAmount.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert the network fee is paid in '$currencySymbol'") {
|
||||
onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||
}
|
||||
step("Sign, send and open the 'Transaction sent' screen") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
openSendSuccessScreenViaLongClickOnSendButton()
|
||||
}
|
||||
}
|
||||
step("Check 'Send via swap' success screen") {
|
||||
checkSendViaSwapSuccessScreen()
|
||||
}
|
||||
step("Click on 'Close' button") {
|
||||
onSendSuccessScreen { closeButton.performClick() }
|
||||
}
|
||||
step("Assert 'Express status' item with title '$expressStatusItemTitle' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -214,33 +214,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
|
||||
if (isHotWalletOnboardingEnabled) {
|
||||
val afterEmptyRoute: AppRoute = if (isHotWalletOnboardingEnabled) {
|
||||
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
|
||||
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
|
||||
if (tangemPayHotWalletOnboardingDeepLink != null) {
|
||||
val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding
|
||||
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
|
||||
val route = if (shouldShowTos) "Disclaimer" else "HotWalletOnboarding"
|
||||
TangemLogger.i("[TangemPay][HWO] TOS accepted=${!shouldShowTos}, navigating to $route")
|
||||
return if (shouldShowTos) {
|
||||
AppRoute.Disclaimer(isTosAccepted = false, nextRoute = hotWalletRoute)
|
||||
} else {
|
||||
hotWalletRoute
|
||||
}
|
||||
AppRoute.TangemPayHotWalletOnboarding
|
||||
} else {
|
||||
getDefaultRoute()
|
||||
}
|
||||
}
|
||||
|
||||
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
// Referral users skip the Home stories screen and land directly on the
|
||||
// mobile wallet creation flow.
|
||||
val afterEmptyRoute: AppRoute = if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
|
||||
} else {
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
getDefaultRoute()
|
||||
}
|
||||
|
||||
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
|
||||
|
|
@ -261,6 +246,19 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getDefaultRoute(): AppRoute {
|
||||
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
// Referral users skip the Home stories screen and land directly on the
|
||||
// mobile wallet creation flow.
|
||||
return if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
|
||||
} else {
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
RootContent(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.tangem.features.onramp.component.*
|
|||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.SendEntryPointComponent
|
||||
|
|
@ -87,6 +88,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val resetCardComponentFactory: ResetCardComponent.Factory,
|
||||
private val referralComponentFactory: ReferralComponent.Factory,
|
||||
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
|
||||
private val pushNotificationSettingsComponentFactory: PushNotificationSettingsComponent.Factory,
|
||||
private val walletComponentFactory: WalletEntryComponent.Factory,
|
||||
private val sendComponentFactoryV2: SendComponent.Factory,
|
||||
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
|
||||
|
|
@ -172,6 +174,13 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = walletSettingsComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.PushNotificationSettings -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = PushNotificationSettingsComponent.Params(route.userWalletId),
|
||||
componentFactory = pushNotificationSettingsComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.WalletBackup -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue