Updated on 2026-08-14
This commit is contained in:
commit
50fe9e603d
1919 changed files with 94940 additions and 15178 deletions
|
|
@ -18,6 +18,7 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
|||
import com.tangem.common.allure.FailedStepScreenshotInterceptor
|
||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME
|
||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.rules.ApiEnvironmentRule
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -179,14 +180,20 @@ abstract class BaseTestCase : TestCase(
|
|||
|
||||
fun waitForIdle() = composeTestRule.waitForIdle()
|
||||
|
||||
/**
|
||||
* Waits until [block] stops throwing (or [timeoutMillis] elapses). Use in scenario (BaseTestCase extension)
|
||||
* code where flakySafely is unavailable; in test bodies prefer flakySafely.
|
||||
*/
|
||||
fun awaitSuccess(timeoutMillis: Long = WAIT_UNTIL_TIMEOUT, block: () -> Unit) {
|
||||
composeTestRule.waitUntil(timeoutMillis = timeoutMillis) { runCatching(block).isSuccess }
|
||||
}
|
||||
|
||||
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
||||
return ApplicationInjectionExecutionRule(
|
||||
toggleStates = mapOf(
|
||||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"GASLESS_APPROVAL_ENABLED" to true,
|
||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
||||
"ASSETS_DISCOVERY_ENABLED" to true,
|
||||
"VISA_ONBOARDING_ENABLED" to true,
|
||||
// Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual
|
||||
|
|
@ -195,20 +202,13 @@ abstract class BaseTestCase : TestCase(
|
|||
// 5.37
|
||||
"HEDERA_ERC20_ENABLED" to true,
|
||||
// 5.39
|
||||
"STAKING_ETH_ENABLED" to true,
|
||||
"DYNAMIC_ADDRESSES_ENABLED" to true,
|
||||
"SOLANA_TX_HISTORY_ENABLED" to true,
|
||||
"SOLANA_SCALED_UI_AMOUNT_ENABLED" to true,
|
||||
"SWAP_AB_ENABLED" to true,
|
||||
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
||||
"AND_15009_SWAP_PROVIDER_FILTER_ENABLED" to true,
|
||||
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
||||
"AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED" to true,
|
||||
"AND_15103_SWAP_RATE_EXPERIENCE_ENABLED" to true,
|
||||
"AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED" to true,
|
||||
"TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED" to true,
|
||||
// 5.39.2
|
||||
"AND_15154_YIELD_PROMO_ENABLED" to true,
|
||||
// 5.40
|
||||
"TWI_1377_MANAGE_FUNDS" to true,
|
||||
// 6.0
|
||||
|
|
@ -223,6 +223,8 @@ abstract class BaseTestCase : TestCase(
|
|||
"AND_15489_EXPRESS_SHARE_BUTTON_ENABLED" to true,
|
||||
"AND_15235_VISA_MULTIPLE_CARDS" to true,
|
||||
"AND_15715_SWAP_BEST_DEX_RATE_ENABLED" to true,
|
||||
// 6.1
|
||||
"TWI_1638_VA_MVP0_ENABLED" to true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ object TestConstants {
|
|||
const val ALLURE_LABEL_VALUE = "Kaspresso"
|
||||
|
||||
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
||||
const val COINS_API_SCENARIO = "coins_api"
|
||||
const val REFERRAL_API_SCENARIO = "referral_api"
|
||||
const val QUOTES_API_SCENARIO = "quotes_api"
|
||||
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"
|
||||
|
|
@ -68,5 +69,7 @@ object TestConstants {
|
|||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
||||
const val TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO = "tangem_pay_eligibility_channels"
|
||||
const val TANGEM_PAY_KYC_STATUS_SCENARIO = "tangem_pay_kyc_status"
|
||||
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AppSettingsScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.test.TopNavigationTestTags
|
||||
|
||||
fun BaseTestCase.tapBackButton() {
|
||||
waitForIdle()
|
||||
composeTestRule.onNode(
|
||||
hasTestTag(TopNavigationTestTags.BACK_BUTTON)
|
||||
or hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
or hasTestTag(TokenDetailsTopBarTestTags.BACK_BUTTON)
|
||||
or hasTestTag(AppSettingsScreenTestTags.BACK_BUTTON),
|
||||
useUnmergedTree = true,
|
||||
).performClick()
|
||||
}
|
||||
|
|
@ -42,6 +42,17 @@ object AddressComparisonHelper {
|
|||
assertTrue(report, false)
|
||||
}
|
||||
|
||||
fun derivationPathForBlockchain(json: String, blockchain: String): String {
|
||||
val array = JSONArray(json)
|
||||
for (i in 0 until array.length()) {
|
||||
val entry = array.getJSONObject(i)
|
||||
if (entry.getString("blockchain").equals(blockchain, ignoreCase = true)) {
|
||||
return entry.getString("derivationPath").trim()
|
||||
}
|
||||
}
|
||||
error("No entry with blockchain '$blockchain' found in addresses JSON")
|
||||
}
|
||||
|
||||
private fun parseAndNormalize(json: String): List<AddressEntry> {
|
||||
val array = JSONArray(json)
|
||||
val entries = mutableListOf<AddressEntry>()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
fun getClipboardText(context: Context): String? {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
|
@ -27,12 +29,15 @@ fun clearClipboard(
|
|||
clipboard.clearPrimaryClip()
|
||||
}
|
||||
|
||||
// Kotlin's assert() is a no-op on device (JVM assertions disabled) — use JUnit asserts here.
|
||||
fun assertClipboardTextEquals(
|
||||
expected: String,
|
||||
context: Context = ApplicationProvider.getApplicationContext()
|
||||
) {
|
||||
assertEquals("Clipboard text mismatch", expected, getClipboardText(context))
|
||||
}
|
||||
|
||||
fun assertClipboardIsEmpty(context: Context = ApplicationProvider.getApplicationContext()) {
|
||||
val actual = getClipboardText(context)
|
||||
assert(actual == expected) {
|
||||
"Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'"
|
||||
}
|
||||
assertTrue("Expected empty clipboard but was: '$actual'", actual.isNullOrEmpty())
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
package com.tangem.common.utils
|
||||
|
||||
import okhttp3.Call
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// WC URIs embed a session symKey that lets anyone join/hijack the session — strip it before logging.
|
||||
|
|
@ -13,6 +19,79 @@ private val WC_SECRET_REGEX = Regex("(symKey(?:=|%3D))[^&\\s\"']+", RegexOption.
|
|||
private fun redactWcSecrets(text: String): String =
|
||||
WC_SECRET_REGEX.replace(text) { "${it.groupValues[1]}<redacted>" }
|
||||
|
||||
private const val DEFAULT_MAX_ATTEMPTS = 4
|
||||
private const val INITIAL_BACKOFF_MS = 500L
|
||||
|
||||
/**
|
||||
* Runs [block] with exponential backoff. Retries ONLY when [block] throws (transient failure:
|
||||
* network error, non-2xx, malformed body). A `null` return is treated as terminal (e.g. "200 but no
|
||||
* data for this key") and is NOT retried. Returns the block result, or null if all attempts failed.
|
||||
*/
|
||||
private fun <T> retryWithBackoff(
|
||||
maxAttempts: Int = DEFAULT_MAX_ATTEMPTS,
|
||||
initialDelayMs: Long = INITIAL_BACKOFF_MS,
|
||||
block: (attempt: Int) -> T,
|
||||
): T? {
|
||||
var delayMs = initialDelayMs
|
||||
var lastError: Throwable? = null
|
||||
repeat(maxAttempts) { i ->
|
||||
val attempt = i + 1
|
||||
try {
|
||||
return block(attempt)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
TangemLogger.w("Attempt $attempt/$maxAttempts failed: ${e.message}")
|
||||
if (attempt < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(delayMs)
|
||||
} catch (ie: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
TangemLogger.w("Retry backoff sleep interrupted; aborting retries")
|
||||
return null
|
||||
}
|
||||
delayMs *= 2
|
||||
}
|
||||
}
|
||||
}
|
||||
TangemLogger.e("All $maxAttempts attempts failed", lastError)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the actual connected endpoint (IPv4/IPv6) and whether a proxy is in the path. Lets CI logs
|
||||
* distinguish "went out via the wrong egress / IPv6 / through a local proxy" from other failures.
|
||||
*/
|
||||
private val diagnosticEventListener = object : EventListener() {
|
||||
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
|
||||
TangemLogger.i("Connecting to ${inetSocketAddress.address?.hostAddress} (proxy=$proxy)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun diagnosticClient(connectSec: Long, readSec: Long, callSec: Long): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(connectSec, TimeUnit.SECONDS)
|
||||
.readTimeout(readSec, TimeUnit.SECONDS)
|
||||
.callTimeout(callSec, TimeUnit.SECONDS)
|
||||
// Don't follow redirects: a Cloudflare Access 302 must stay visible (its `location` points at
|
||||
// cloudflareaccess.com) so logHttpFailure can flag "egress IP not allow-listed". /health and
|
||||
// /addresses have no legitimate redirects.
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.eventListener(diagnosticEventListener)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Logs enough to classify a failed response at a glance: `cf-ray`/`location`/`server` reveal a
|
||||
* Cloudflare Access 302 (egress IP not allow-listed) vs an origin 5xx vs anything else.
|
||||
*/
|
||||
private fun logHttpFailure(tag: String, response: Response, body: String) {
|
||||
TangemLogger.e(
|
||||
"$tag failed: code=${response.code} cf-ray=${response.header("cf-ray") ?: "-"} " +
|
||||
"server=${response.header("server") ?: "-"} location=${response.header("location") ?: "-"} " +
|
||||
"body=${body.take(200)}",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a WalletConnect URI from the qa-tools service.
|
||||
*
|
||||
|
|
@ -142,50 +221,40 @@ fun getAddressesFromApi(
|
|||
): String? {
|
||||
TangemLogger.i("Getting addresses for seed key: $seedKey")
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.callTimeout(90, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
val client = diagnosticClient(connectSec = 30, readSec = 60, callSec = 90)
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/addresses")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
return retryWithBackoff { attempt ->
|
||||
TangemLogger.i("Getting addresses for '$seedKey', attempt $attempt")
|
||||
client.newCall(request).execute().use { response ->
|
||||
TangemLogger.i("Response code: ${response.code}")
|
||||
if (!response.isSuccessful) {
|
||||
// Transient (network/Access 302/5xx) — throw so retryWithBackoff retries.
|
||||
logHttpFailure("getAddressesFromApi", response, response.body?.string() ?: "")
|
||||
throw IOException("getAddressesFromApi: HTTP ${response.code}")
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
val body = response.body?.string() ?: ""
|
||||
val contentType = response.header("Content-Type") ?: ""
|
||||
if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) {
|
||||
logHttpFailure("getAddressesFromApi (not JSON)", response, body)
|
||||
throw IOException("getAddressesFromApi: unexpected non-JSON response")
|
||||
}
|
||||
|
||||
val contentType = response.header("Content-Type") ?: ""
|
||||
if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) {
|
||||
TangemLogger.e("Unexpected response (not JSON), Content-Type: $contentType, body: $body")
|
||||
return null
|
||||
}
|
||||
|
||||
val jsonObject = JSONObject(body)
|
||||
val data = jsonObject.optJSONObject("data") ?: jsonObject
|
||||
val seedData = data.optJSONArray(seedKey)
|
||||
|
||||
if (seedData != null) {
|
||||
TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries")
|
||||
seedData.toString()
|
||||
} else {
|
||||
TangemLogger.e("No data found for seed key: $seedKey")
|
||||
null
|
||||
}
|
||||
val jsonObject = JSONObject(body)
|
||||
val data = jsonObject.optJSONObject("data") ?: jsonObject
|
||||
val seedData = data.optJSONArray(seedKey)
|
||||
if (seedData != null) {
|
||||
TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries")
|
||||
seedData.toString()
|
||||
} else {
|
||||
val errorBody = response.body?.string() ?: "No error body"
|
||||
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
|
||||
// Terminal: server responded fine but has no data for this key — retrying won't help.
|
||||
TangemLogger.e("No data found for seed key: $seedKey")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error getting addresses", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,43 +263,31 @@ fun checkServiceHealth(
|
|||
): String? {
|
||||
TangemLogger.i("Checking service health")
|
||||
|
||||
val client = OkHttpClient()
|
||||
val client = diagnosticClient(connectSec = 15, readSec = 30, callSec = 45)
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/health")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
return retryWithBackoff { attempt ->
|
||||
TangemLogger.i("Checking service health, attempt $attempt")
|
||||
client.newCall(request).execute().use { response ->
|
||||
TangemLogger.i("Response code: ${response.code}")
|
||||
if (!response.isSuccessful) {
|
||||
// Transient (network/Access 302/5xx) — throw so retryWithBackoff retries.
|
||||
logHttpFailure("checkServiceHealth", response, response.body?.string() ?: "")
|
||||
throw IOException("checkServiceHealth: HTTP ${response.code}")
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
TangemLogger.i("Response body: $body")
|
||||
|
||||
if (body.isEmpty()) {
|
||||
TangemLogger.e("Response body is empty")
|
||||
return null
|
||||
}
|
||||
|
||||
val jsonObject = JSONObject(body)
|
||||
val status = jsonObject.optString("status", "")
|
||||
|
||||
if (status.isNotEmpty()) {
|
||||
TangemLogger.i("Got status successfully: $status")
|
||||
status
|
||||
} else {
|
||||
TangemLogger.e("Status field is missing or empty")
|
||||
null
|
||||
}
|
||||
val body = response.body?.string() ?: ""
|
||||
val status = if (body.isEmpty()) "" else JSONObject(body).optString("status", "")
|
||||
if (status.isNotEmpty()) {
|
||||
TangemLogger.i("Got status successfully: $status")
|
||||
status
|
||||
} else {
|
||||
val errorBody = response.body?.string() ?: "No error body"
|
||||
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
|
||||
null
|
||||
// Empty/malformed body from a 2xx — treat as transient and retry.
|
||||
logHttpFailure("checkServiceHealth (empty status)", response, body)
|
||||
throw IOException("checkServiceHealth: missing 'status' field")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error checking health", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.common.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.qrcode.QRCodeReader
|
||||
|
||||
/** Decodes the text encoded in a QR-code [bitmap] (e.g. captured from a Compose node via captureToImage). */
|
||||
fun decodeQrCode(bitmap: Bitmap): String {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
val hints = mapOf(DecodeHintType.TRY_HARDER to true)
|
||||
|
||||
return QRCodeReader().decode(binaryBitmap, hints).text
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||
|
|
@ -57,31 +56,6 @@ fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private const val TESTER_MENU_MAX_ATTEMPTS = 3
|
||||
|
||||
/**
|
||||
* Presses 'Volume Down' twice to open tester menu.
|
||||
* Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if the menu doesn't appear.
|
||||
*/
|
||||
private fun BaseTestCase.openTesterMenu() {
|
||||
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
|
||||
waitForIdle()
|
||||
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
|
||||
val opened = runCatching {
|
||||
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
|
||||
}.isSuccess
|
||||
|
||||
if (opened) {
|
||||
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
|
||||
return
|
||||
}
|
||||
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
|
||||
}
|
||||
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS].
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,19 @@ fun BaseTestCase.openMainScreen(
|
|||
}
|
||||
}
|
||||
|
||||
/** Opens the main screen, synchronizes addresses, and opens the details of the token with [tokenName]. */
|
||||
fun BaseTestCase.openTokenDetails(tokenName: String) {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessCode: String = "") {
|
||||
step("Click on 'Get started' button") {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,14 @@ fun BaseTestCase.enterAmountAndOpenSendConfirm(amount: String, recipientAddress:
|
|||
* On the 'Send confirm' screen, open the network-fee selector and switch the fee token from the
|
||||
* native coin to the given (stablecoin) token — the core gasless action repeated across the suite.
|
||||
*/
|
||||
fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String) {
|
||||
fun BaseTestCase.selectStablecoinAsFeeToken(
|
||||
coinName: String,
|
||||
tokenName: String,
|
||||
// Positive flows wait until 'Apply' is enabled (fee is computed & payable). Negative flows
|
||||
// (insufficient balance) must NOT wait for that — 'Apply' stays disabled — so pass false and let
|
||||
// the caller assert the disabled/error state itself.
|
||||
expectApplyEnabled: Boolean = true,
|
||||
) {
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorBlock.assertIsDisplayed()
|
||||
|
|
@ -57,6 +64,17 @@ fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String)
|
|||
step("Select '$tokenName' as the fee-paying token") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
step("Wait until the '$tokenName' fee is loaded") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching {
|
||||
onSendFeeSelectorBottomSheet {
|
||||
networkFeeTitle.assertIsDisplayed()
|
||||
feeTokenItem(tokenName).assertIsDisplayed()
|
||||
if (expectApplyEnabled) applyButton.assertIsEnabled()
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||
import com.tangem.screens.onAddCustomTokenScreen
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onManageTokensScreen
|
||||
import com.tangem.screens.onWalletSettingsScreen
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
|
||||
|
||||
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
|
||||
openWalletSettingsScreen()
|
||||
openAccountDetails(accountName)
|
||||
step("Click on 'Manage tokens' button") {
|
||||
onAccountDetailsScreen { manageTokensButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openAddCustomToken(accountName: String = mainAccountName()) {
|
||||
openManageTokens(accountName)
|
||||
step("Click on 'Add custom token' button") {
|
||||
onManageTokensScreen { addCustomTokenButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: String, derivationPath: String) {
|
||||
step("Click on network: '$network'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(network) } }
|
||||
onAddCustomTokenScreen { networkRow(network).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$contract'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(contract) }
|
||||
}
|
||||
step("Click on 'Derivation path' field") {
|
||||
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
onAddCustomTokenScreen { derivationSelectorField.performClick() }
|
||||
}
|
||||
step("Click on 'Custom derivation' button") {
|
||||
awaitSuccess { onAddCustomTokenScreen { customDerivationButton.assertIsDisplayed() } }
|
||||
onAddCustomTokenScreen { customDerivationButton.performClick() }
|
||||
}
|
||||
step("Enter custom derivation path: '$derivationPath'") {
|
||||
awaitSuccess { onDialog { inputField.assertIsDisplayed() } }
|
||||
onDialog { inputField.performTextInputInChunks(derivationPath) }
|
||||
awaitSuccess { onDialog { okButton.assertIsEnabled() } }
|
||||
onDialog { okButton.performClick() }
|
||||
}
|
||||
step("Click on 'Add token' button") {
|
||||
awaitSuccess { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
onAddCustomTokenScreen { addTokenButton.performClick() }
|
||||
}
|
||||
navigateBackToMainFromManageTokens()
|
||||
}
|
||||
|
||||
private fun BaseTestCase.navigateBackToWalletSettings() {
|
||||
step("Click on 'Manage tokens' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onManageTokensScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Account details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onAccountDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.assertDerivationPathsInSelector(networkToOpen: String, vararg expectedPaths: Pair<String, String>) {
|
||||
step("Click on network: '$networkToOpen'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(networkToOpen) } }
|
||||
onAddCustomTokenScreen { networkRow(networkToOpen).performClick() }
|
||||
}
|
||||
step("Click on 'Derivation path' field") {
|
||||
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
onAddCustomTokenScreen { derivationSelectorField.performClick() }
|
||||
}
|
||||
expectedPaths.forEach { (networkId, path) ->
|
||||
step("Assert '$networkId' derivation path is '$path'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToDerivationRow(networkId) } }
|
||||
onAddCustomTokenScreen { derivationPath(networkId, path).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.toggleTokenNetworkInManageTokens(tokenTitle: String, networkTitle: String) {
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
awaitSuccess { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Click on '$networkTitle' switch") {
|
||||
awaitSuccess { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.navigateBackToMainFromManageTokens() {
|
||||
navigateBackToWalletSettings()
|
||||
step("Click on 'Wallet settings' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onWalletSettingsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -86,13 +86,23 @@ fun BaseTestCase.addMissingReceiveTokenToWallet(token: String, recipientWalletNa
|
|||
runCatching { onSwapSelectTokenScreen { marketsTokenWithName(token).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
// The 'Add token' sheet pre-selects the recipient (the only wallet missing the token, since the source already holds it).
|
||||
step("Assert recipient wallet '$recipientWalletName' is selected") {
|
||||
step("Select recipient wallet '$recipientWalletName'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { walletName(recipientWalletName).assertIsDisplayed() } }.isSuccess
|
||||
runCatching { onAddToPortfolioScreen { selectedWalletRow.performClick() } }.isSuccess
|
||||
}
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { walletOption(recipientWalletName).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddToPortfolioScreen { addButton.performClick() }
|
||||
step("Select network '$token'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { networkRow.performClick() } }.isSuccess
|
||||
}
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { networkOption(token).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddToPortfolioScreen { confirmButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,58 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.extractText
|
||||
import com.tangem.common.utils.decodeQrCode
|
||||
import com.tangem.screens.onAddFundsBottomSheet
|
||||
import com.tangem.screens.onReceiveAssetsBottomSheet
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
|
||||
import com.tangem.screens.onTokenReceiveWarningBottomSheet
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import org.junit.Assert
|
||||
|
||||
/** Opens the receive flow from a funded token's details via 'Add funds' → 'Receive'. */
|
||||
fun BaseTestCase.openReceiveViaAddFunds() {
|
||||
step("Click on 'Add funds' button") {
|
||||
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Receive' button in bottom sheet") {
|
||||
onAddFundsBottomSheet { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the QR code encodes the displayed address for both address types of a two-address-type coin,
|
||||
* and that the two addresses differ.
|
||||
*/
|
||||
fun BaseTestCase.assertQrCodesMatchForBothAddressTypes() {
|
||||
step("Go to QR code bottom sheet for the first address type") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
var firstAddress = ""
|
||||
step("Assert QR code encodes the first displayed address") {
|
||||
firstAddress = assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
step("Go back to the receive addresses") {
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Switch to the second address type") {
|
||||
awaitSuccess(WAIT_UNTIL_TIMEOUT_LONG) { onReceiveAssetsBottomSheet { addressesPager.assertIsDisplayed() } }
|
||||
onReceiveAssetsBottomSheet { scrollToAddress(1) }
|
||||
}
|
||||
step("Click on 'Show QR code' button for the second address type") {
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton(1).clickWithAssertion() }
|
||||
}
|
||||
var secondAddress = ""
|
||||
step("Assert QR code encodes the second displayed address") {
|
||||
secondAddress = assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
step("Assert the two address types are different") {
|
||||
Assert.assertNotEquals(firstAddress, secondAddress)
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.goToQrCodeBottomSheet() {
|
||||
step("Assert 'Token receive warning' bottom sheet is displayed") {
|
||||
|
|
@ -15,7 +62,7 @@ fun BaseTestCase.goToQrCodeBottomSheet() {
|
|||
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
|
||||
}
|
||||
step("Click on 'Show QR code' button") {
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton().clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,3 +86,16 @@ fun BaseTestCase.checkQrCodeBottomSheetScenario() {
|
|||
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Decodes the QR code on the receive bottom sheet and asserts it encodes the displayed address; returns that address. */
|
||||
fun BaseTestCase.assertQrCodeEncodesDisplayedAddress(): String {
|
||||
var displayedAddress = ""
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
onTokenReceiveQrCodeBottomSheet {
|
||||
displayedAddress = address.extractText()
|
||||
val decoded = decodeQrCode(captureQrCodeBitmap())
|
||||
Assert.assertEquals(displayedAddress, decoded)
|
||||
}
|
||||
}
|
||||
return displayedAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onMainScreenTopBar
|
||||
import com.tangem.screens.onReferralProgramScreen
|
||||
import com.tangem.screens.onWalletSettingsScreen
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
fun BaseTestCase.referralTakeParticipate() {
|
||||
step("Open 'Details' screen") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Referral program' screen") {
|
||||
onWalletSettingsScreen { referralProgramButton.clickWithAssertion() }
|
||||
}
|
||||
step("Tap 'Participate'") {
|
||||
onReferralProgramScreen { participateButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify referral code is displayed") {
|
||||
onReferralProgramScreen { personalCodeCard.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
|
@ -317,6 +317,9 @@ fun BaseTestCase.openSwapInTransferModeWithHotWallet(
|
|||
}
|
||||
|
||||
private fun BaseTestCase.navigateToSwapForToken(tokenName: String, fromAccountName: String) {
|
||||
step("Collapse header") {
|
||||
onMainScreen { collapseHeader() }
|
||||
}
|
||||
step("Scroll '$fromAccountName' into view (semantics, not touch — avoids the Markets sheet)") {
|
||||
onMainScreen { scrollToAccount(fromAccountName) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import android.view.KeyEvent
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.screens.onTesterMenuScreen
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
private const val TESTER_MENU_MAX_ATTEMPTS = 3
|
||||
|
||||
/**
|
||||
* Opens the tester menu by sending two 'Volume Down' key events in a single shell command.
|
||||
* VolumeButtonDoublePressObserver needs two ACTION_DOWN within 300ms; two separate pressKeyCode
|
||||
* calls are too slow, but both events in one `input keyevent` invocation arrive back-to-back within
|
||||
* the window. Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if it doesn't appear.
|
||||
*/
|
||||
fun BaseTestCase.openTesterMenu() {
|
||||
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
|
||||
waitForIdle()
|
||||
val volumeDown = KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
device.uiDevice.executeShellCommand("input keyevent $volumeDown $volumeDown")
|
||||
|
||||
val opened = runCatching {
|
||||
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
|
||||
}.isSuccess
|
||||
|
||||
if (opened) {
|
||||
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
|
||||
return
|
||||
}
|
||||
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
|
||||
}
|
||||
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AddCustomTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
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.res.R as CoreResR
|
||||
|
||||
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val selectorList: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
|
||||
}
|
||||
|
||||
fun networkRow(networkName: String): KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.networkRow(networkName))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToNetwork(networkName: String) = selectorList {
|
||||
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName)))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToDerivationRow(networkId: String) = selectorList {
|
||||
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId)))
|
||||
}
|
||||
|
||||
fun derivationPath(networkId: String, path: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
hasTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId))
|
||||
hasAnyDescendant(withText(path))
|
||||
}
|
||||
|
||||
val contractAddressField: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
|
||||
}
|
||||
|
||||
val derivationSelectorField: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.DERIVATION_SELECTOR_FIELD)
|
||||
}
|
||||
|
||||
val customDerivationButton: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.CUSTOM_DERIVATION_BUTTON)
|
||||
}
|
||||
|
||||
val warningNotification: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.WARNING_NOTIFICATION)
|
||||
}
|
||||
|
||||
val addTokenButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(CoreResR.string.common_add_token))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddCustomTokenScreen(function: AddCustomTokenPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -1,24 +1,53 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
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
|
||||
|
||||
// Scoped to the bottom-sheet container: the swap token-search screen stays behind the sheet and
|
||||
// would otherwise make text matches (wallet tabs, market 'Ethereum' item) ambiguous.
|
||||
class AddToPortfolioPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddToPortfolioPageObject>(semanticsProvider = semanticsProvider) {
|
||||
ComposeScreen<AddToPortfolioPageObject>(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||
) {
|
||||
|
||||
fun walletName(walletName: String): KNode = child {
|
||||
hasText(walletName)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
/** The wallet row on the 'Add token' screen (label + selected value); tap to open the wallet selector. */
|
||||
val selectedWalletRow: KNode = child {
|
||||
hasText(getResourceString(R.string.wc_common_wallet))
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** A wallet item inside the portfolio selector screen. */
|
||||
fun walletOption(walletName: String): KNode = child {
|
||||
hasText(walletName)
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** The network row on the 'Add token' screen; tap to open the 'Choose network' selector. */
|
||||
val networkRow: KNode = child {
|
||||
hasText(getResourceString(R.string.wc_common_network))
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** A network item inside the 'Choose network' selector. */
|
||||
fun networkOption(networkName: String): KNode = child {
|
||||
hasText(networkName)
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_confirm))
|
||||
hasClickAction()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractions
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
hasText(getResourceString(R.string.common_confirm))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ class AppSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val backButton: KNode = child {
|
||||
hasTestTag(AppSettingsScreenTestTags.BACK_BUTTON)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
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.core.res.R as CoreResR
|
||||
|
||||
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val screenContainer: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_CONTAINER)
|
||||
}
|
||||
|
||||
val topAppBarBackButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
|
@ -61,6 +66,12 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasAnyDescendant(withText(name))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val getTangemPayRow: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
hasAnyDescendant(withText(getResourceString(CoreResR.string.tangempay_get_tangem_pay)))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.screens
|
|||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseDialogTestTags
|
||||
|
|
@ -10,6 +11,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
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
|
||||
|
||||
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -18,6 +20,12 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasTestTag(BaseDialogTestTags.CONTAINER)
|
||||
}
|
||||
|
||||
fun containerWithText(text: String): KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.CONTAINER)
|
||||
hasAnyDescendant(withText(text = text, substring = true))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.TITLE)
|
||||
}
|
||||
|
|
@ -87,6 +95,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasText(getResourceString(R.string.common_ok))
|
||||
}
|
||||
|
||||
val forgetButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(CoreResR.string.common_forget))
|
||||
}
|
||||
|
||||
val changeButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_change))
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
* Required because TangemCollapsingTopBar places the body at y=collapsingHeight, which
|
||||
* pushes lower list items off-screen when the header is expanded.
|
||||
*/
|
||||
private fun collapseHeader() {
|
||||
fun collapseHeader() {
|
||||
screenContainer {
|
||||
performTouchInput { swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) }
|
||||
}
|
||||
|
|
@ -139,12 +139,13 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
}
|
||||
|
||||
val walletImportedBanner: KNode = child {
|
||||
hasTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.initial_wallet_sync_banner_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val walletImportedBannerCheckHereButton: KNode = child {
|
||||
hasAnyAncestor(withTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER))
|
||||
hasAnyAncestor(withTestTag(NotificationTestTags.CONTAINER))
|
||||
hasText(getResourceString(CoreResR.string.main_manage_tokens))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
|
@ -201,6 +202,12 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val getTangemPayBanner: KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_onboarding_banner_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val devCardNotificationIcon: KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title)))
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.R
|
|||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
||||
import com.tangem.core.ui.test.SearchBarTestTags
|
||||
import com.tangem.core.ui.test.SwitchTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
|
|
@ -17,6 +18,7 @@ import androidx.compose.ui.test.hasText as withText
|
|||
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
||||
import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant
|
||||
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -35,11 +37,43 @@ class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
}
|
||||
|
||||
val searchClearButton: KNode = child {
|
||||
hasTestTag(SearchBarTestTags.CLEAR_BUTTON)
|
||||
}
|
||||
|
||||
val addCustomTokenButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
|
||||
}
|
||||
|
||||
fun tokenItem(tokenName: String): KNode = child {
|
||||
hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM)
|
||||
hasText(tokenName)
|
||||
}
|
||||
|
||||
fun networkStandard(networkName: String, standard: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
withText(standard).and(
|
||||
withAnyAncestor(
|
||||
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
|
||||
.and(withAnyDescendant(withText(networkName, ignoreCase = true))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun networkName(networkName: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
|
||||
.and(withAnyDescendant(withText(networkName))),
|
||||
)
|
||||
}
|
||||
|
||||
val contractAddressCopiedMessage: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.contract_address_copied_message))
|
||||
}
|
||||
|
||||
fun networkSwitch(networkName: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.TokenReceiveAssetsBottomSheetTestTags
|
||||
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
|
||||
|
|
@ -11,10 +13,23 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
|
|||
class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val showQrCodeButton: KNode = child {
|
||||
/** 'Show QR code' button of the address at [index]; both pager cards stay composed, so match by position. */
|
||||
fun showQrCodeButton(index: Int = 0): KNode = child {
|
||||
hasText(getResourceString(R.string.token_receive_show_qr_code_title))
|
||||
hasPosition(index)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addressesPager: KNode = child {
|
||||
hasTestTag(TokenReceiveAssetsBottomSheetTestTags.ADDRESSES_PAGER)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/** Pages the addresses carousel to the address of the given [index]. */
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToAddress(index: Int) {
|
||||
addressesPager { performScrollToIndex(index) }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val personalCodeCard: KNode = child {
|
||||
hasTestTag(ReferralProgramScreenTestTags.PERSONAL_CODE_CARD)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.ui.test.BaseButtonTestTags
|
|||
import com.tangem.core.ui.test.NotificationTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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
|
||||
|
|
@ -62,6 +63,33 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
hasText(getResourceString(R.string.staking_enabled))
|
||||
}
|
||||
|
||||
val yieldSupplyAvailableBlock: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_AVAILABLE_BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val yieldSupplyActiveBlock: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val earnBlockTitleIcon: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.EARN_BLOCK_TITLE_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val yieldModeConnectedTitle: KNode = child {
|
||||
hasAnyAncestor(withTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK))
|
||||
hasText(getResourceString(CoreResR.string.yield_module_transaction_enter))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun yieldModeApy(apy: String): KNode = child {
|
||||
hasAnyAncestor(withTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK))
|
||||
hasText(getResourceString(CoreResR.string.yield_module_average_apy, apy))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||
}
|
||||
|
|
@ -90,6 +118,11 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/** 'Receive' row of the zero-balance actions block (Buy / Swap / Receive), shown instead of the action buttons. */
|
||||
val receiveButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_receive))
|
||||
}
|
||||
|
||||
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName)))
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
|
|
@ -114,6 +147,12 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenTitle(name: String): KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||
hasAnyDescendant(withText(text = name, substring = true))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun networkFeeNotificationMessage(
|
||||
currencyName: String,
|
||||
networkName: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TokenMarketBlockTestTags
|
||||
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
|
||||
|
||||
class TokenMarketBlockPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TokenMarketBlockPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val block: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val price: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.PRICE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val priceChange: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.PRICE_CHANGE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val chart: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.CHART)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTokenMarketBlock(function: TokenMarketBlockPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.captureToImage
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
|
||||
import com.tangem.wallet.R
|
||||
import io.github.kakaocup.compose.intercept.operation.ComposeOperationType
|
||||
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
|
||||
|
|
@ -40,16 +43,25 @@ class TokenReceiveQrCodeBottomSheetPageObject(semanticsProvider: SemanticsNodeIn
|
|||
}
|
||||
|
||||
val copyButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_copy))
|
||||
useUnmergedTree = true
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
val shareButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_share))
|
||||
useUnmergedTree = true
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** Captures the QR code node as a bitmap via the Kakao node delegate (no raw composeTestRule access). */
|
||||
fun captureQrCodeBitmap(): Bitmap {
|
||||
lateinit var bitmap: Bitmap
|
||||
qrCode.delegate.perform(QrCodeAction.CAPTURE) {
|
||||
bitmap = captureToImage().asAndroidBitmap()
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private enum class QrCodeAction : ComposeOperationType { CAPTURE }
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTokenReceiveQrCodeBottomSheet(function: TokenReceiveQrCodeBottomSheetPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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.TokenReceiveWarningBottomSheetTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -18,9 +17,7 @@ class TokenReceiveWarningBottomSheetPageObject(semanticsProvider: SemanticsNodeI
|
|||
}
|
||||
|
||||
val gotItButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_got_it))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ class TxHistoryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionUnconfirmedStatus(title: String): KNode = transactionItem(title).child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.STATUS_UNCONFIRMED)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionAddress(title: String, address: String): KNode = transactionItem(title).child {
|
||||
hasText(text = address, substring = true)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.NotificationTestTags
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 YieldSupplyActivePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyActivePageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val stopEarningButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.STOP_EARNING_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val approveButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(CoreResR.string.yield_module_approve_needed_notification_cta))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun notificationTitle(title: String): KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(title)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyActiveScreen(function: YieldSupplyActivePageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyApprovePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyApprovePageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.APPROVE_CONFIRM_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyApproveScreen(function: YieldSupplyApprovePageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyPromoPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyPromoPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val continueButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.PROMO_CONTINUE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyPromoScreen(function: YieldSupplyPromoPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyStartEarningPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyStartEarningPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val startEarningButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.START_EARNING_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyStartEarningScreen(function: YieldSupplyStartEarningPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyStopEarningPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyStopEarningPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.STOP_EARNING_CONFIRM_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyStopEarningScreen(function: YieldSupplyStopEarningPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
||||
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 androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class TangemPayKycSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayKycSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
fun title(text: String): KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.TITLE)
|
||||
hasText(text)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun primaryButtonWithText(text: String): KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||
hasAnyDescendant(withText(text))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val closeButton: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.CLOSE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayKycSheet(function: TangemPayKycSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
|||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -19,6 +20,12 @@ class TangemPayMainPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tileWithSubtitle(subtitle: String): KNode = child {
|
||||
hasTestTag(TangemPayTestTags.MAIN_SCREEN_TILE)
|
||||
hasAnyDescendant(withText(subtitle))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val balance: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE)
|
||||
useUnmergedTree = true
|
||||
|
|
@ -41,6 +48,15 @@ class TangemPayMainPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val moreActionsButton: KNode = child {
|
||||
hasTestTag(TokenDetailsTopBarTestTags.MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val termsAndFeesMenuItem: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangem_pay_terms_limits))
|
||||
}
|
||||
|
||||
fun transactionRowWithText(text: String): KNode = child {
|
||||
hasText(text)
|
||||
useUnmergedTree = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayOnboardingPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayOnboardingPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_onboarding_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayOnboardingScreen(function: TangemPayOnboardingPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayServiceUnavailableSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayServiceUnavailableSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_service_unavailable_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val description: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.MESSAGE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_service_unavailable_description))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
// Merged tree: BUTTON_PRIMARY tags the button, its label is a child — only the merged node has both.
|
||||
val gotItButton: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||
hasText(getResourceString(CoreResR.string.common_got_it))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayServiceUnavailableSheet(
|
||||
function: TangemPayServiceUnavailableSheetPageObject.() -> Unit,
|
||||
) = onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -59,15 +59,15 @@ class AppCurrencyTest : BaseTestCase() {
|
|||
onAppSettingsScreen { currencyButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to 'Details' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
step("Return to 'Details' screen via 'Back' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onAppSettingsScreen { backButton.clickWithAssertion() }
|
||||
onDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to 'Main' screen via 'Back' button") {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Main' screen is opened") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,13 @@ 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
|
||||
class BlockchainTest : BaseTestCase() {
|
||||
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("3644")
|
||||
@DisplayName("ADA: Checking the min amount/change = 1ADA")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.extensions.clickAndWaitFor
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.card.ScanFailsRequester
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
|
|
@ -196,4 +197,68 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
@AllureId("3960")
|
||||
@DisplayName("Send feedback: Failed card scanning on Details screen")
|
||||
@Test
|
||||
fun sendFeedbackFailedScanningOnDetails() {
|
||||
val gmailText = "Welcome to Gmail"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
device.uiDevice.pressBack()
|
||||
MockProvider.resetEmulateError()
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Details screen'") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Set scanning error (the next card scan will fail with TagLost)") {
|
||||
MockProvider.setEmulateError(TangemSdkError.TagLost())
|
||||
}
|
||||
step("Click 'Add new wallet' button") {
|
||||
onDetailsScreen { addWalletButton.clickWithAssertion() }
|
||||
}
|
||||
step("Force show 'Scan warning' dialog"){
|
||||
waitForIdle()
|
||||
runOnUiThread {
|
||||
MainScope().launch {
|
||||
scanFailsRequester.show(AnalyticsParam.ScreensSources.Main)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Check 'Scan warning' dialog") {
|
||||
waitForIdle()
|
||||
checkScanWarningDialog()
|
||||
}
|
||||
step("Click on 'Request support' button") {
|
||||
onScanWarningDialog { requestSupportButton.performClick() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3603")
|
||||
@DisplayName("Send feedback: S2C card has Contact support option")
|
||||
@Test
|
||||
fun sendFeedbackForS2CTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Start2Coin)
|
||||
}
|
||||
step("Open 'Details screen'") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify 'Contact support' button is displayed") {
|
||||
onDetailsScreen { contactSupportButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@ class SendTest : BaseTestCase() {
|
|||
val currencyName = "USDC"
|
||||
val feeCurrencyName = "Solana"
|
||||
val feeCurrencySymbol = "SOL"
|
||||
val balanceScenarioName = "solana_balance"
|
||||
val balanceScenarioName = "solana_get_account_info_recipient"
|
||||
val tokensScenarioName = "user_tokens_api"
|
||||
val balanceState = "Empty"
|
||||
val balanceState = "ZeroBalance"
|
||||
val tokensState = "SolanaUSDC"
|
||||
|
||||
setupHooks(
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
package com.tangem.tests.actionButtons
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
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
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.pullToRefresh
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
||||
import com.tangem.scenarios.enterAmountAndOpenSendConfirm
|
||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openSendFromTokenDetails
|
||||
import com.tangem.scenarios.openSendScreenWithHotWallet
|
||||
import com.tangem.scenarios.openSendSuccessScreenViaLongClickOnSendButton
|
||||
import com.tangem.scenarios.readNetworkFeeAmount
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.scenarios.waitUntilNetworkFeeIsStable
|
||||
import com.tangem.screens.onAddFundsBottomSheet
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onSendScreen
|
||||
import com.tangem.screens.onSendSuccessScreen
|
||||
import com.tangem.screens.onSwapStoriesScreen
|
||||
import com.tangem.screens.onSwapTokenScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTransferBottomSheet
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
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
|
||||
|
|
@ -28,6 +45,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
@AllureId("594")
|
||||
@DisplayName("Action buttons (token details screen): validate UI")
|
||||
@Test
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
fun actionButtonsValidateUiTest() {
|
||||
val tokenTitle = "Bitcoin"
|
||||
|
||||
|
|
@ -113,8 +131,8 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Assert 'Buy' button in bottom sheet is enabled") {
|
||||
onAddFundsBottomSheet { buyButton.assertIsEnabled() }
|
||||
}
|
||||
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||
onAddFundsBottomSheet { swapButton.assertIsNotEnabled() }
|
||||
step("Assert 'Swap' button in bottom sheet is not displayed") {
|
||||
onAddFundsBottomSheet { swapButton.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert 'Receive' button in bottom sheet is enabled") {
|
||||
onAddFundsBottomSheet { receiveButton.assertIsEnabled() }
|
||||
|
|
@ -257,4 +275,146 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4465")
|
||||
@DisplayName("Action buttons (token details screen): 'Send' blocked while a transaction is active, works after completion")
|
||||
@Test
|
||||
fun sendBlockedWhileTransactionActiveTest() {
|
||||
val tokenName = "XRP Ledger"
|
||||
val amount = "1"
|
||||
val userTokensState = "XRPHotWalletSvS"
|
||||
val quotesState = "Ripple"
|
||||
val startedState = "Started"
|
||||
val rippleAccountInfoScenario = "ripple_account_info"
|
||||
val pendingSendMessagePrefix =
|
||||
getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send).substringBefore("%")
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(rippleAccountInfoScenario)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$rippleAccountInfoScenario' to state: '$startedState'") {
|
||||
setWireMockScenarioState(scenarioName = rippleAccountInfoScenario, state = startedState)
|
||||
}
|
||||
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||
}
|
||||
step("Enter amount '$amount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = amount, recipientAddress = XRP_RECIPIENT_ADDRESS)
|
||||
}
|
||||
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||
step("Sign, send and open the 'Transaction sent' screen") {
|
||||
openSendSuccessScreenViaLongClickOnSendButton()
|
||||
}
|
||||
step("Click on 'Close' button") {
|
||||
onSendSuccessScreen { closeButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Open the transfer bottom sheet") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Send' button is not enabled while the transaction is active") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.performClick() }
|
||||
}
|
||||
step("Assert pending-transaction notification dialog is displayed") {
|
||||
onDialog { containerWithText(pendingSendMessagePrefix).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Send' screen is not opened") {
|
||||
onSendScreen { amountInputTextField.assertDoesNotExist() }
|
||||
}
|
||||
// Tapping the 'Send' row dismisses the transfer bottom sheet (onActionDispatched) before the dialog shows.
|
||||
step("Close the notification dialog") {
|
||||
onDialog { okButton.clickWithAssertion() }
|
||||
}
|
||||
step("Pull to refresh to complete the active transaction") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Open the transfer bottom sheet again") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Send' button is enabled after the transaction is completed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTransferBottomSheet { sendButton.assertIsEnabled() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.performClick() }
|
||||
}
|
||||
step("Assert 'Send' screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendScreen { amountInputTextField.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10209")
|
||||
@DisplayName("Action buttons (token details screen): 'Send' unavailable for a zero-balance token with an active transaction")
|
||||
@Test
|
||||
fun sendUnavailableForZeroBalanceWithActiveTransactionTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val zeroBalanceState = "ZeroBalance"
|
||||
val activeTxHistoryState = "UnconfirmedOutgoing"
|
||||
val balanceScenarioName = "dogecoin_balance"
|
||||
val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
val sendingTitle = getResourceString(R.string.common_sending)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenarioName)
|
||||
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: '$balanceScenarioName' to state: '$zeroBalanceState'") {
|
||||
setWireMockScenarioState(scenarioName = balanceScenarioName, state = zeroBalanceState)
|
||||
}
|
||||
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$activeTxHistoryState'") {
|
||||
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = activeTxHistoryState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing '$sendingTitle' transaction block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Transfer' button is not displayed for the zero-balance token") {
|
||||
onTokenDetailsScreen { transferButton.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.addFunds
|
|||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.assertTextContainsSafe
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddTokenBottomSheet
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.screens.onBuyTokenDetailsScreen
|
|||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.Issue
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -66,8 +68,8 @@ class BuyTest : BaseTestCase() {
|
|||
step("Click on $token in Trending list") {
|
||||
onAddFundsBottomSheet { trendingTokenWithTitle(token).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.clickWithAssertion() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Close 'Get token' screen") {
|
||||
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||
|
|
@ -83,4 +85,24 @@ class BuyTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3613")
|
||||
@DisplayName("On-ramp Buy: S2C card doesn't have Buy and Sell options")
|
||||
@Test
|
||||
@Issue("[REDACTED_TASK_KEY]")
|
||||
fun buyAndSellIsNotAvailableForS2CCardTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen(productType = ProductType.Start2Coin)
|
||||
}
|
||||
step("Verify 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify Buy/Sell action buttons are hidden") {
|
||||
onMainScreen {
|
||||
buyButton.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,9 +86,9 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
|||
step("Click on 'Add' button in 'Markets' bottom sheet") {
|
||||
onMarketsScreen { addButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Add' button in 'Add token' bottom sheet") {
|
||||
step("Click on 'Confirm' button in 'Add token' bottom sheet") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
}
|
||||
step("Press 'Back' button") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.addCustomTokenWithCustomDerivation
|
||||
import com.tangem.scenarios.assertDerivationPathsInSelector
|
||||
import com.tangem.scenarios.navigateBackToMainFromManageTokens
|
||||
import com.tangem.scenarios.openAddCustomToken
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddCustomTokenScreen
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet1LegacyDerivationMockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class AddCustomTokenTest : BaseTestCase() {
|
||||
|
||||
private val richState = "ManageTokensRich"
|
||||
private val tokenTitle = "Tether"
|
||||
private val ethereumNetwork = "Ethereum"
|
||||
private val solanaNetwork = "Solana"
|
||||
private val bitcoinNetworkId = "bitcoin"
|
||||
private val ethereumClassicNetworkId = "ethereum-classic"
|
||||
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
||||
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
||||
private val customDerivationPath = "m/44'/60'/0'/0/1"
|
||||
|
||||
@AllureId("772")
|
||||
@DisplayName("Add custom token: added token appears on Main")
|
||||
@Test
|
||||
fun addCustomTokenAppearsOnMainTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$ethereumNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(ethereumNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(ethereumNetwork).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$ethContract'") {
|
||||
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
// Chunked input spans several validation ticks — the model drops the first sampled form change.
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(ethContract) }
|
||||
}
|
||||
step("Click on 'Add token' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
onAddCustomTokenScreen { addTokenButton.performClick() }
|
||||
}
|
||||
step("Navigate back to 'Main Screen'") { navigateBackToMainFromManageTokens() }
|
||||
step("Assert token: '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("775")
|
||||
@DisplayName("Add custom token: custom derivation path is accepted")
|
||||
@Test
|
||||
fun addCustomTokenWithCustomDerivationTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Add custom token with custom derivation on '$ethereumNetwork'") {
|
||||
addCustomTokenWithCustomDerivation(
|
||||
network = ethereumNetwork,
|
||||
contract = ethContract,
|
||||
derivationPath = customDerivationPath,
|
||||
)
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("771")
|
||||
@DisplayName("Add custom token: custom derivation indicator is shown on Main")
|
||||
@Test
|
||||
fun customDerivationIndicatorOnMainTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Add custom token with custom derivation on '$ethereumNetwork'") {
|
||||
addCustomTokenWithCustomDerivation(
|
||||
network = ethereumNetwork,
|
||||
contract = ethContract,
|
||||
derivationPath = customDerivationPath,
|
||||
)
|
||||
}
|
||||
step("Assert custom derivation indicator for '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithCustomDerivationIcon(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("770")
|
||||
@DisplayName("Add custom token: derivation field is available for a non-EVM network")
|
||||
@Test
|
||||
fun derivationAvailableForNonEvmNetworkTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$solanaNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
|
||||
}
|
||||
step("Assert 'Derivation path' field is available") {
|
||||
flakySafely { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("769")
|
||||
@DisplayName("Add custom token: Solana token on a modern card shows no unsupported warning")
|
||||
@Test
|
||||
fun solanaTokenModernCardNoWarningTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$solanaNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$solanaContract'") {
|
||||
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(solanaContract) }
|
||||
}
|
||||
step("Assert 'Add token' button is enabled") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
}
|
||||
step("Assert unsupported-token warning is not displayed") {
|
||||
onAddCustomTokenScreen { warningNotification.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("777")
|
||||
@DisplayName("Add custom token: network with a missing curve is not offered")
|
||||
@Test
|
||||
fun missingCurveNetworkNotOfferedTest() {
|
||||
val bitcoinNetwork = "Bitcoin"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert supported network '$bitcoinNetwork' is displayed") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(bitcoinNetwork) } }
|
||||
}
|
||||
step("Assert '$solanaNetwork' network is not displayed for the missing-curve card") {
|
||||
onAddCustomTokenScreen { selectorList.assertIsDisplayed() }
|
||||
val solanaOffered = runCatching { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }.isSuccess
|
||||
assertFalse("'$solanaNetwork' should not be offered for a missing-curve card", solanaOffered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("776")
|
||||
@DisplayName("Add custom token: derivation paths match a legacy-batch V1 Wallet card")
|
||||
@Test
|
||||
fun derivationPathsLegacyBatchWalletTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet1LegacyDerivationMockContent) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V1 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/44'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10204")
|
||||
@DisplayName("Add custom token: derivation paths match a V2 Wallet card")
|
||||
@Test
|
||||
fun derivationPathsWalletTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V2 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/44'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/60'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10205")
|
||||
@DisplayName("Add custom token: derivation paths match a V3 Wallet 2 card")
|
||||
@Test
|
||||
fun derivationPathsWallet2Test() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(productType = ProductType.Wallet2) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V3 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/84'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import androidx.compose.ui.test.longClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.assertClipboardIsEmpty
|
||||
import com.tangem.common.utils.assertClipboardTextEquals
|
||||
import com.tangem.common.utils.clearClipboard
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openManageTokens
|
||||
import com.tangem.scenarios.toggleTokenNetworkInManageTokens
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onManageTokensScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Firmware451MockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
|
||||
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
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
@HiltAndroidTest
|
||||
class ManageTokensTest : BaseTestCase() {
|
||||
|
||||
private val richState = "ManageTokensRich"
|
||||
private val solanaTokenTitle = "USD Coin"
|
||||
private val solanaNetworkTitle = "SOLANA"
|
||||
private val solanaName = "Solana"
|
||||
|
||||
@AllureId("765")
|
||||
@DisplayName("Manage tokens: network standard labels are shown for token networks")
|
||||
@Test
|
||||
fun networkStandardLabelsTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val ethereum = "Ethereum"
|
||||
val bnbSmartChain = "BNB Smart Chain"
|
||||
val tron = "Tron"
|
||||
val erc20 = "ERC20"
|
||||
val bep20 = "BEP20"
|
||||
val trc20 = "TRC20"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Assert '$ethereum' network standard '$erc20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen { networkStandard(networkName = ethereum, standard = erc20).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$bnbSmartChain' network standard '$bep20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen {
|
||||
networkStandard(networkName = bnbSmartChain, standard = bep20).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert '$tron' network standard '$trc20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen { networkStandard(networkName = tron, standard = trc20).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("667")
|
||||
@DisplayName("Manage tokens: search by name and ticker filters the list")
|
||||
@Test
|
||||
fun searchByNameAndTickerTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val nameQuery = "Tether"
|
||||
val tickerQuery = "USDT"
|
||||
val emptyQuery = "Zzqnotoken"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Search by name: '$nameQuery'") {
|
||||
onManageTokensScreen {
|
||||
searchField.performClick()
|
||||
searchField.performTextInput(nameQuery)
|
||||
}
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
step("Clear search field") {
|
||||
onManageTokensScreen { searchClearButton.clickWithAssertion() }
|
||||
}
|
||||
step("Search by ticker: '$tickerQuery'") {
|
||||
onManageTokensScreen { searchField.performTextInput(tickerQuery) }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
step("Clear search field") {
|
||||
onManageTokensScreen { searchClearButton.clickWithAssertion() }
|
||||
}
|
||||
step("Search by unknown query: '$emptyQuery'") {
|
||||
onManageTokensScreen { searchField.performTextInput(emptyQuery) }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is not displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertDoesNotExist() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("763")
|
||||
@DisplayName("Manage tokens: enabling Solana network on a modern card shows no warning")
|
||||
@Test
|
||||
fun solanaNetworkNoWarningTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert hide-token alert is not displayed") {
|
||||
waitForIdle()
|
||||
onDialog { dialogContainer.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("719")
|
||||
@DisplayName("Manage tokens: long tap on a token network copies the contract address, main network does not")
|
||||
@Test
|
||||
fun copyContractAddressOnNetworkLongTapTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val tokenNetworkTitle = "ETHEREUM"
|
||||
val coinTitle = "Bitcoin"
|
||||
val coinNetworkTitle = "BITCOIN"
|
||||
val contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Long click on '$tokenNetworkTitle' network row") {
|
||||
flakySafely { onManageTokensScreen { networkName(tokenNetworkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen {
|
||||
networkName(tokenNetworkTitle).performTouchInput {
|
||||
longClick(position = center, durationMillis = 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert contract-address-copied message is displayed") {
|
||||
flakySafely { onManageTokensScreen { contractAddressCopiedMessage.assertIsDisplayed() } }
|
||||
}
|
||||
step("Assert clipboard contains '$tokenTitle' contract address") {
|
||||
assertClipboardTextEquals(contractAddress)
|
||||
}
|
||||
step("Clear clipboard") { clearClipboard() }
|
||||
step("Click on token: '$coinTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(coinTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(coinTitle).performClick() }
|
||||
}
|
||||
step("Long click on '$coinNetworkTitle' main network row") {
|
||||
flakySafely { onManageTokensScreen { networkName(coinNetworkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen {
|
||||
networkName(coinNetworkTitle).performTouchInput {
|
||||
longClick(position = center, durationMillis = 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert clipboard is empty after long click on main network") {
|
||||
waitForIdle()
|
||||
assertClipboardIsEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("737")
|
||||
@DisplayName("Manage tokens: enabling Solana on an old-firmware card shows firmware-limitation warning")
|
||||
@Test
|
||||
fun solanaFirmwareLimitationWarningTest() {
|
||||
val warningMessage = getResourceString(CoreResR.string.alert_manage_tokens_unsupported_message, solanaName)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Firmware451MockContent) }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert firmware-limitation warning is displayed") {
|
||||
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("767")
|
||||
@DisplayName("Manage tokens: enabling Solana on a card missing its curve shows unsupported-curve warning")
|
||||
@Test
|
||||
fun solanaUnsupportedCurveWarningTest() {
|
||||
val warningMessage =
|
||||
getResourceString(CoreResR.string.alert_manage_tokens_unsupported_curve_message, solanaName)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert unsupported-curve warning is displayed") {
|
||||
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,9 @@ class WarningsTest : BaseTestCase() {
|
|||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
package com.tangem.tests.referral
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.tapBackButton
|
||||
import com.tangem.common.utils.AddressComparisonHelper
|
||||
import com.tangem.common.utils.getClipboardText
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openTesterMenu
|
||||
import com.tangem.scenarios.referralTakeParticipate
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onMainScreenTopBar
|
||||
import com.tangem.screens.onTesterMenuScreen
|
||||
import com.tangem.screens.onWalletSettingsScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class ReferralTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3630")
|
||||
@DisplayName("Referral program: Token and Blockchain added on Main screen after participation")
|
||||
@Test
|
||||
fun referralTokenAndBlockchainAddedAfterParticipationTest() {
|
||||
val tokenNetwork = "Tron"
|
||||
val token = "Tether"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarios()
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Verify network $tokenNetwork and token $token is not displayed") {
|
||||
onMainScreen {
|
||||
assertTokenDoesNotExist(tokenNetwork)
|
||||
assertTokenDoesNotExist(token)
|
||||
}
|
||||
}
|
||||
step("Take participate in Referral program") {
|
||||
referralTakeParticipate()
|
||||
}
|
||||
step("Return to the 'Main' screen") {
|
||||
tapBackButton()
|
||||
onWalletSettingsScreen { screenContainer.assertIsDisplayed() }
|
||||
tapBackButton()
|
||||
onDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
tapBackButton()
|
||||
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify network $tokenNetwork and token $token is displayed on 'Main' screen") {
|
||||
onMainScreen {
|
||||
tokenWithTitleAndAddress(tokenNetwork)
|
||||
tokenWithTitleAndAddress(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10098")
|
||||
@DisplayName("Referral program: Token added on Main screen after participation")
|
||||
@Test
|
||||
fun referralTokenAddedAfterParticipationTest() {
|
||||
val userWalletScenarioName = "user_tokens_api"
|
||||
val userWalletState = "Tron"
|
||||
val tokenNetwork = "Tron"
|
||||
val token = "Tether"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarios()
|
||||
}
|
||||
).run {
|
||||
step("Set wiremock scenario: $userWalletScenarioName to state $userWalletState") {
|
||||
setWireMockScenarioState(scenarioName = userWalletScenarioName, state = userWalletState)
|
||||
}
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Verify token $token is not displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(token) }
|
||||
}
|
||||
step("Verify network $tokenNetwork is displayed") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenNetwork) }
|
||||
}
|
||||
step("Take participate in Referral program") {
|
||||
referralTakeParticipate()
|
||||
}
|
||||
step("Return to the 'Main' screen") {
|
||||
tapBackButton()
|
||||
onWalletSettingsScreen { screenContainer.assertIsDisplayed() }
|
||||
tapBackButton()
|
||||
onDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
tapBackButton()
|
||||
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify $token is displayed on 'Main' screen") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3629")
|
||||
@DisplayName("Referral program: Participating unavailable for No-Wallet cards")
|
||||
@Test
|
||||
fun referralUnavailableForNoWalletCardsTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen(productType = ProductType.Note)
|
||||
}
|
||||
step("Open 'Details' screen") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify 'Referral program' button is not displayed") {
|
||||
onWalletSettingsScreen { referralProgramButton.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3636")
|
||||
@DisplayName("Referral program: Verify wallet derivation")
|
||||
@Test
|
||||
fun referralVerifyWalletDerivationTest() {
|
||||
val tokenNetwork = "Tron"
|
||||
val expectedDerivationPath = "m/44'/195'/0'/0/0"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarios()
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Take participate in Referral program") {
|
||||
referralTakeParticipate()
|
||||
}
|
||||
step("Open Debug menu") {
|
||||
openTesterMenu()
|
||||
}
|
||||
step("Open 'Addresses info' screen") {
|
||||
onTesterMenuScreen { addressesInfoButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify $tokenNetwork derivation path is '$expectedDerivationPath'") {
|
||||
onTesterMenuScreen { jsonTab.clickWithAssertion() }
|
||||
onTesterMenuScreen { copyButton.clickWithAssertion() }
|
||||
val addressesJson = getClipboardText(ApplicationProvider.getApplicationContext())
|
||||
?: error("Clipboard is empty after copying addresses")
|
||||
val actualDerivationPath =
|
||||
AddressComparisonHelper.derivationPathForBlockchain(addressesJson, tokenNetwork)
|
||||
assertEquals(expectedDerivationPath, actualDerivationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import org.junit.Test
|
|||
@HiltAndroidTest
|
||||
class SendConfirmScreenTest : BaseTestCase() {
|
||||
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("4003")
|
||||
@DisplayName("Send (Confirm screen): change sending amount")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ class GaslessSendTest : BaseTestCase() {
|
|||
@DisplayName("Gasless: completed gasless transaction is shown in token transaction history")
|
||||
@Test
|
||||
fun checkGaslessTransactionInHistoryTest() {
|
||||
val sentAmount = "1.00"
|
||||
val operationAmount = "1.00"
|
||||
val gaslessFeeAmount = "0.10"
|
||||
val sentTitle = getResourceString(R.string.common_sent)
|
||||
val operationTitle = getResourceString(R.string.transaction_history_operation)
|
||||
val gaslessFeeTitle = getResourceString(R.string.gasless_transaction_fee)
|
||||
|
||||
setupHooks(
|
||||
|
|
@ -166,13 +166,13 @@ class GaslessSendTest : BaseTestCase() {
|
|||
onTxHistoryScreen { transactionItem(gaslessFeeTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$sentTitle' transaction is displayed") {
|
||||
onTxHistoryScreen { transactionItem(sentTitle).assertIsDisplayed() }
|
||||
step("Assert '$operationTitle' transaction is displayed") {
|
||||
onTxHistoryScreen { transactionItem(operationTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$sentTitle' amount '$sentAmount' is displayed in '$currencySymbol'") {
|
||||
step("Assert '$operationTitle' amount '$operationAmount' is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(sentTitle).assertTextContains(sentAmount, substring = true)
|
||||
transactionCurrency(sentTitle).assertTextEquals(currencySymbol)
|
||||
transactionAmount(operationTitle).assertTextContains(operationAmount, substring = true)
|
||||
transactionCurrency(operationTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert gasless '$gaslessFeeTitle' amount '$gaslessFeeAmount' is displayed in '$currencySymbol'") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.tests.send.reasonBlock
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTransferBottomSheet
|
||||
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 ReasonBlockTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3616")
|
||||
@DisplayName("Reason block: Send is unavailable if user has pending transaction")
|
||||
@Test
|
||||
fun reasonBlockSendUnavailableWithPendingTransactionTest() {
|
||||
val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
val txHistoryState = "EmptyWithPendingTransaction"
|
||||
val walletsScenarioName = "user_tokens_api"
|
||||
val walletsState = "Dogecoin"
|
||||
val token = "Dogecoin"
|
||||
val reasonText = getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send)
|
||||
.substringBefore("%s")
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(txHistoryScenarioName)
|
||||
resetWireMockScenarioState(walletsScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set Wiremock scenario: $txHistoryScenarioName to state $txHistoryState") {
|
||||
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryState)
|
||||
}
|
||||
step("Set Wiremock scenario: $walletsScenarioName to state $walletsState") {
|
||||
setWireMockScenarioState(scenarioName = walletsScenarioName, state = walletsState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name $token") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Transfer' button") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify 'Send' button is disabled") {
|
||||
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert pending-transaction reason dialog is displayed") {
|
||||
onDialog { containerWithText(reasonText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3615")
|
||||
@DisplayName("Reason block: Token withdrawal is unavailable if there are no fee coverage")
|
||||
@Test
|
||||
fun reasonBlockTokenWithdrawalUnavailableWithoutFeeCoverage() {
|
||||
val userWalletsScenarioName = "user_tokens_api"
|
||||
val userWalletsState = "SolanaUSDC"
|
||||
val solBalanceScenarioName = "solana_get_account_info_recipient"
|
||||
val solBalanceState = "ZeroBalance"
|
||||
val token = "USDC"
|
||||
val feeCurrencyName = "Solana"
|
||||
val feeCurrencySymbol = "SOL"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(userWalletsScenarioName)
|
||||
resetWireMockScenarioState(solBalanceScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set Wiremock scenario: $userWalletsScenarioName to state $userWalletsState") {
|
||||
setWireMockScenarioState(scenarioName = userWalletsScenarioName, state = userWalletsState)
|
||||
}
|
||||
step("Set Wiremock scenario: $solBalanceScenarioName to state $solBalanceState") {
|
||||
setWireMockScenarioState(scenarioName = solBalanceScenarioName, state = solBalanceState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name $token") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Insufficient $feeCurrencySymbol for fee' notification is displayed") {
|
||||
onTokenDetailsScreen {
|
||||
networkFeeNotificationTitle(feeCurrencyName).assertIsDisplayed()
|
||||
networkFeeNotificationMessage(
|
||||
currencyName = token,
|
||||
networkName = feeCurrencyName,
|
||||
feeCurrencyName = feeCurrencyName,
|
||||
feeCurrencySymbol = feeCurrencySymbol,
|
||||
).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Go to $feeCurrencySymbol' button") {
|
||||
onTokenDetailsScreen { goToBuyCurrencyButton(feeCurrencySymbol).clickWithAssertion() }
|
||||
}
|
||||
step("Assert $feeCurrencyName token screen is opened") {
|
||||
onTokenDetailsScreen { tokenTitle(feeCurrencyName).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +155,11 @@ class GaslessSendViaSwapTest : BaseTestCase() {
|
|||
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
selectStablecoinAsFeeToken(
|
||||
coinName = nativeTokenName,
|
||||
tokenName = tokenName,
|
||||
expectApplyEnabled = false,
|
||||
)
|
||||
}
|
||||
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.common.extensions.extractText
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
|
||||
|
|
@ -825,4 +826,112 @@ class SendViaSwapTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3972")
|
||||
@DisplayName("Send via Swap: single-currency card has no 'Convert' button in Send")
|
||||
@Test
|
||||
fun singleCurrencyCardNoConvertButtonInSendTest() {
|
||||
val cardType = ProductType.Note
|
||||
val tokenTitle = "Dogecoin"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on '${cardType.name}' card") {
|
||||
openMainScreen(cardType)
|
||||
}
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Transfer' button is displayed") {
|
||||
onMainScreen { transferButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Swap' button is not displayed") {
|
||||
onMainScreen { swapButton.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Transfer' button") {
|
||||
onMainScreen { transferButton.performClick() }
|
||||
}
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
onChooseTokenBottomSheet { tokenWithTitle(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Send' button in bottom sheet") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert amount input text field is displayed") {
|
||||
flakySafely { onSendScreen { amountInputTextField.assertIsDisplayed() } }
|
||||
}
|
||||
step("Assert 'Swap to another token' button is not displayed") {
|
||||
onSendScreen { swapToAnotherTokenButton.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10196")
|
||||
@DisplayName("Send via Swap: memo-restricted network is not selectable as a receive token")
|
||||
@Test
|
||||
fun memoRestrictedNetworkNotSelectableAsReceiveTokenTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val algorand = "Algorand"
|
||||
val bitcoinBalanceScenarioName = "bitcoin_utxo"
|
||||
val bitcoinBalanceScenarioState = "Balance"
|
||||
val assetsScenarioName = "express_api_assets"
|
||||
val assetsScenarioState = "BitcoinExchangeEnabled"
|
||||
val userTokensScenarioState = "Wallet2"
|
||||
val coinsScenarioName = "coins_api"
|
||||
val coinsScenarioState = "WithAlgorand"
|
||||
val warningTitle = getResourceString(R.string.express_swap_not_supported_title, algorand)
|
||||
val warningMessage = getResourceString(R.string.express_swap_not_supported_text)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(bitcoinBalanceScenarioName)
|
||||
resetWireMockScenarioState(assetsScenarioName)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(coinsScenarioName)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$bitcoinBalanceScenarioName' to state: '$bitcoinBalanceScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = bitcoinBalanceScenarioName, state = bitcoinBalanceScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$coinsScenarioName' to state: '$coinsScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = coinsScenarioName, state = coinsScenarioState)
|
||||
}
|
||||
step("Open 'Send' screen") {
|
||||
openSendScreen(tokenName, mockContent = Wallet2WithDerivationsMockContent)
|
||||
}
|
||||
step("Click on 'Swap to another token' button") {
|
||||
onSendScreen { swapToAnotherTokenButton.performClick() }
|
||||
}
|
||||
step("Type '$algorand' in search text field") {
|
||||
onSendViaSwapScreen {
|
||||
searchField.performClick()
|
||||
searchField.performTextInput(algorand)
|
||||
}
|
||||
}
|
||||
step("Click on token: '$algorand'") {
|
||||
onSendViaSwapScreen { tokenItem(algorand).performClick() }
|
||||
}
|
||||
step("Assert warning bottom sheet icon is displayed") {
|
||||
onWarningBottomSheet { icon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning bottom sheet title is displayed") {
|
||||
onWarningBottomSheet { title(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning bottom sheet message is displayed") {
|
||||
onWarningBottomSheet { message(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Ok, Got it!' button") {
|
||||
onWarningBottomSheet { okGotItButton.performClick() }
|
||||
}
|
||||
step("Assert 'Swap to another token' screen is displayed") {
|
||||
onSendViaSwapScreen { tokenItem(algorand).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ 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.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.scenarios.checkSendWarning
|
||||
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||
import com.tangem.scenarios.openSendScreen
|
||||
import com.tangem.screens.onSendAddressScreen
|
||||
import com.tangem.screens.onSendScreen
|
||||
|
|
@ -90,8 +92,10 @@ class SolanaWarningsTest : BaseTestCase() {
|
|||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
step("Open 'Send confirm' screen via 'Next' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
openSendConfirmScreenViaNextButton()
|
||||
}
|
||||
}
|
||||
step("Assert 'Invalid amount warning' is not displayed") {
|
||||
checkSendWarning(
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
@ -106,8 +106,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
@ -195,8 +195,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert warning title '$warningTitle' is displayed") {
|
||||
onSwapTokenScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
|
|
@ -251,8 +251,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(swapTokenName).performClick() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
|||
val tokenTitle = "USDC"
|
||||
val inputAmount = "1000"
|
||||
val tokensScenarioState = "SolanaUSDC"
|
||||
val balanceScenarioName = "solana_balance"
|
||||
val balanceScenarioState = "Empty"
|
||||
val balanceScenarioName = "solana_get_account_info_recipient"
|
||||
val balanceScenarioState = "ZeroBalance"
|
||||
val pairsScenarioName = "solana_from_pairs"
|
||||
val pairsScenarioState = "DexProvider"
|
||||
val networkName = "Solana"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ACCESS_CODE
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_KYC_STATUS_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_SHORT
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onMainScreenTopBar
|
||||
import com.tangem.screens.tangempay.onTangemPayKycSheet
|
||||
import com.tangem.screens.tangempay.onTangemPayMainScreen
|
||||
import com.tangem.screens.tangempay.onTangemPayOnboardingScreen
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayOnboardingKycTest : BaseTestCase() {
|
||||
|
||||
@AllureId("9630")
|
||||
@DisplayName("Tangem Pay: existing customer authorization shows the tile on Main")
|
||||
@Test
|
||||
fun existingUserAuthorizationShowsTangemPayTileOnMainTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile is displayed on Main") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { mainScreenTile.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9632")
|
||||
@DisplayName("Tangem Pay: Get Tangem Pay banner is shown on Main for the BANNER channel")
|
||||
@Test
|
||||
fun getTangemPayBannerShownOnMainWhenBannerChannelReceivedTest() {
|
||||
val eligibilityStartedState = "Started"
|
||||
val channelsBannerState = "Banner"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$eligibilityStartedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityStartedState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO' to '$channelsBannerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO, channelsBannerState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert 'Get Tangem Pay' banner is displayed on Main") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { getTangemPayBanner.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9673")
|
||||
@DisplayName("Tangem Pay: onboarding opens from app Details for the DETAILS channel")
|
||||
@Test
|
||||
fun onboardingOpensFromAppDetailsWhenDetailsChannelReceivedTest() {
|
||||
val eligibilityStartedState = "Started"
|
||||
val channelsBannerAndDetailsState = "BannerAndDetails"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$eligibilityStartedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityStartedState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO' to '$channelsBannerAndDetailsState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO, channelsBannerAndDetailsState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Click on 'More' button") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Get Tangem Pay' row is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onDetailsScreen { getTangemPayRow.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Get Tangem Pay' row") {
|
||||
onDetailsScreen { getTangemPayRow.clickWithAssertion() }
|
||||
}
|
||||
step("Assert Tangem Pay onboarding screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayOnboardingScreen { title.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9643")
|
||||
@DisplayName("Tangem Pay: KYC in progress sheet closes and the status remains on Main")
|
||||
@Test
|
||||
fun kycInProgressSheetClosesAndStatusRemainsOnMainTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
val kycInProgressState = "InProgress"
|
||||
val kycInProgressText = getResourceString(CoreResR.string.tangempay_kyc_in_progress)
|
||||
val viewStatusText = getResourceString(CoreResR.string.tangempay_kyc_in_progress_notification_button)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_KYC_STATUS_SCENARIO' to '$kycInProgressState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO, kycInProgressState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycInProgressText' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycInProgressText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on Tangem Pay tile") {
|
||||
onTangemPayMainScreen { mainScreenTile.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC in progress sheet is displayed") {
|
||||
onTangemPayKycSheet {
|
||||
title(kycInProgressText).assertIsDisplayed()
|
||||
primaryButtonWithText(viewStatusText).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Close' button") {
|
||||
onTangemPayKycSheet { closeButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC in progress sheet is not displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_SHORT) {
|
||||
onTangemPayKycSheet { primaryButtonWithText(viewStatusText).assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycInProgressText' is still displayed") {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycInProgressText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9517")
|
||||
@DisplayName("Tangem Pay: KYC rejected status shown on Main and in the sheet")
|
||||
@Test
|
||||
fun kycRejectedStatusShownOnMainAndInSheetTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
val kycDeclinedState = "Declined"
|
||||
val kycRejectedTileText = getResourceString(CoreResR.string.tangempay_kyc_has_failed)
|
||||
val rejectedSheetTitle = getResourceString(CoreResR.string.tangempay_kyc_rejected)
|
||||
val goToSupportText = getResourceString(CoreResR.string.tangempay_go_to_support)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_KYC_STATUS_SCENARIO' to '$kycDeclinedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO, kycDeclinedState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycRejectedTileText' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycRejectedTileText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on Tangem Pay tile") {
|
||||
onTangemPayMainScreen { mainScreenTile.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC rejected sheet is displayed") {
|
||||
onTangemPayKycSheet {
|
||||
title(rejectedSheetTitle).assertIsDisplayed()
|
||||
primaryButtonWithText(goToSupportText).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
|
|
@ -11,11 +18,14 @@ import com.tangem.common.utils.assertClipboardTextEquals
|
|||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.features.tangempay.TangemPayConstants
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.tangempay.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -240,4 +250,75 @@ class TangemPayTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9592")
|
||||
@DisplayName("Tangem Pay: Terms and fees opens the tariffs document")
|
||||
@Test
|
||||
fun termsAndFeesOpensTariffsFromMoreActionsMenuTest() {
|
||||
val eligibilityState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay") { openTangemPay() }
|
||||
step("Click on 'More actions' button") {
|
||||
onTangemPayMainScreen { moreActionsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Stub external browser so the tariffs document is not actually opened") {
|
||||
intending(hasAction(Intent.ACTION_VIEW))
|
||||
.respondWith(ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
step("Click on 'Terms and fees' menu item") {
|
||||
onTangemPayMainScreen { termsAndFeesMenuItem.clickWithAssertion() }
|
||||
}
|
||||
step("Assert the tariffs document is opened in an external browser") {
|
||||
intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(TangemPayConstants.TERMS_AND_LIMITS_LINK)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iOS-only: Android has no 'Service temporarily unavailable' sheet and no empty-depositAddress guard in Add funds.
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("9557")
|
||||
@DisplayName("Tangem Pay: service unavailable error when customer has no deposit address")
|
||||
@Test
|
||||
fun addFundsShowsServiceUnavailableErrorWhenNoDepositAddressTest() {
|
||||
val depositAddressScenario = "tangem_pay_deposit_address"
|
||||
val noDepositAddressState = "NoDepositAddress"
|
||||
val eligibilityState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(depositAddressScenario, noDepositAddressState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(depositAddressScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay") { openTangemPay() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onTangemPayMainScreen { topUpButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Service temporarily unavailable' sheet is displayed") {
|
||||
onTangemPayServiceUnavailableSheet {
|
||||
title.assertIsDisplayed()
|
||||
description.assertIsDisplayed()
|
||||
gotItButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Got it' button") {
|
||||
onTangemPayServiceUnavailableSheet { gotItButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Service temporarily unavailable' sheet is not displayed") {
|
||||
onTangemPayServiceUnavailableSheet { title.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
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.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.assertQrCodeEncodesDisplayedAddress
|
||||
import com.tangem.scenarios.assertQrCodesMatchForBothAddressTypes
|
||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||
import com.tangem.scenarios.openReceiveViaAddFunds
|
||||
import com.tangem.scenarios.openTokenDetails
|
||||
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 TokenDetailsAddressesTest : BaseTestCase() {
|
||||
|
||||
@AllureId("4947")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Bitcoin, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressBitcoinTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10218")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Cosmos)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressCosmosTest() {
|
||||
val tokenName = "Cosmos"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
},
|
||||
).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: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10219")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Kaspa)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressKaspaTest() {
|
||||
val tokenName = "Kaspa"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
val quotesKaspaState = "Kaspa"
|
||||
val kaspaUtxoScenario = "kaspa_utxo"
|
||||
val kaspaUtxoState = "more_than_84_android"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(kaspaUtxoScenario)
|
||||
},
|
||||
).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: '$quotesKaspaState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesKaspaState)
|
||||
}
|
||||
step("Set WireMock scenario: '$kaspaUtxoScenario' to state: '$kaspaUtxoState'") {
|
||||
setWireMockScenarioState(scenarioName = kaspaUtxoScenario, state = kaspaUtxoState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10215")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Litecoin, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressLitecoinTest() {
|
||||
val tokenName = "Litecoin"
|
||||
val userTokensState = "Litecoin"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' button") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10220")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (XDC Network, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressXdcTest() {
|
||||
val tokenName = "XDC Network"
|
||||
val userTokensState = "XDC"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' button") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10216")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Hedera)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressHederaTest() {
|
||||
val tokenName = "Hedera"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
},
|
||||
).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: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10217")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Ethereum)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressEthereumTest() {
|
||||
val tokenName = "Ethereum"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10214")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Decimal Smart Chain, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressDecimalTest() {
|
||||
val tokenName = "Decimal Smart Chain"
|
||||
val userTokensState = "Decimal"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' action") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
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.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenMarketBlock
|
||||
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 TokenDetailsMarketPriceTests : BaseTestCase() {
|
||||
|
||||
@AllureId("301")
|
||||
@DisplayName("Token details: Market Price block data")
|
||||
@Test
|
||||
fun marketPriceBlockDataTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val marketPriceTitle = getResourceString(R.string.markets_common_market_price)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).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("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Market Price' block is displayed with title '$marketPriceTitle'") {
|
||||
onTokenMarketBlock {
|
||||
block.assertIsDisplayed()
|
||||
title.assertTextEquals(marketPriceTitle)
|
||||
}
|
||||
}
|
||||
step("Assert price rate is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenMarketBlock { price.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 24h price change is displayed") {
|
||||
onTokenMarketBlock { priceChange.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert mini chart is displayed") {
|
||||
onTokenMarketBlock { chart.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
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.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
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 TokenDetailsTests : BaseTestCase() {
|
||||
|
||||
private val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
|
||||
@AllureId("304")
|
||||
@DisplayName("Token details: active outgoing transaction block")
|
||||
@Test
|
||||
fun activeOutgoingTransactionBlockTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val currencySymbol = "DOGE"
|
||||
val txHistoryScenarioState = "UnconfirmedOutgoing"
|
||||
val sendingTitle = getResourceString(R.string.common_sending)
|
||||
val recipientBriefAddress = DOGECOIN_RECIPIENT_ADDRESS.toBriefAddressFormat()
|
||||
|
||||
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("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing '$sendingTitle' transaction is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert active outgoing transaction status is unconfirmed") {
|
||||
onTxHistoryScreen { transactionUnconfirmedStatus(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing transaction amount is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(sendingTitle).assertIsDisplayed()
|
||||
transactionCurrency(sendingTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert active outgoing transaction recipient address '$recipientBriefAddress' is displayed") {
|
||||
onTxHistoryScreen { transactionAddress(sendingTitle, recipientBriefAddress).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
|
|||
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
|
|
|
|||
|
|
@ -0,0 +1,548 @@
|
|||
package com.tangem.tests.yield
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.longClick
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.pullToRefresh
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenDetailsTopBar
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
import com.tangem.screens.onYieldSupplyActiveScreen
|
||||
import com.tangem.screens.onYieldSupplyApproveScreen
|
||||
import com.tangem.screens.onYieldSupplyPromoScreen
|
||||
import com.tangem.screens.onYieldSupplyStartEarningScreen
|
||||
import com.tangem.screens.onYieldSupplyStopEarningScreen
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@HiltAndroidTest
|
||||
class YieldModeTest : BaseTestCase() {
|
||||
|
||||
@AllureId("7957")
|
||||
@DisplayName("Yield mode: first-time landing activation with zero balance")
|
||||
@Test
|
||||
fun firstTimeLandingActivationZeroBalanceTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val apy = "5.24"
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereumZeroBalance"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "ZeroUsdcEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4938")
|
||||
@DisplayName("Yield mode: first-time landing activation")
|
||||
@Test
|
||||
fun firstTimeLandingActivationTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val apy = "5.24"
|
||||
val enterTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_enter)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen {
|
||||
transactionItem(enterTransactionTitle).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5473")
|
||||
@DisplayName("Yield mode: top-up for active landing")
|
||||
@Test
|
||||
fun topUpActiveLandingTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val topUpAmount = "600.00"
|
||||
val receivedTransactionTitle = getResourceString(CoreResR.string.common_received)
|
||||
val topUpTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_topup)
|
||||
val supplyingNotificationTitle = getResourceString(
|
||||
CoreResR.string.yield_module_amount_not_transfered_to_aave_title,
|
||||
topUpAmount,
|
||||
tokenTitle,
|
||||
)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldTopUpState = "TopUp"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldTopUpState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldTopUpState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Not supplied' info icon is displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Received' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(receivedTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Supply to Aave' transaction is displayed in history") {
|
||||
onTxHistoryScreen { transactionItem(topUpTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Supplying to Aave' notification is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyActiveScreen { notificationTitle(supplyingNotificationTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("7960")
|
||||
@DisplayName("Yield mode: granting approval")
|
||||
@Test
|
||||
fun grantApprovalTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val approveNeededTitle = getResourceString(CoreResR.string.yield_module_approve_needed_notification_title)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldApproveNeededState = "Active"
|
||||
val yieldApproveGrantedState = "ApproveGranted"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldApproveNeededState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldApproveNeededState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Approve needed' info icon is displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Approve needed' notification is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyActiveScreen { notificationTitle(approveNeededTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Approve' button") {
|
||||
onYieldSupplyActiveScreen { approveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Confirm' button to confirm approval") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyApproveScreen {
|
||||
confirmButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
confirmButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldApproveGrantedState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldApproveGrantedState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Approve needed' info icon is not displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4940")
|
||||
@DisplayName("Yield mode: reopening landing")
|
||||
@Test
|
||||
fun reopenLandingActivationTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val nativeCoinTitle = "Ethereum"
|
||||
val apy = "5.24"
|
||||
val reactivateTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_reactivate)
|
||||
val enterTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_enter)
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(enterTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click 'Back' button to return to 'Main Screen'") {
|
||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on native coin with name: '$nativeCoinTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(nativeCoinTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Reactivate Token' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(reactivateTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Enter protocol' transaction is displayed in history") {
|
||||
onTxHistoryScreen { transactionItem(enterTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4937")
|
||||
@DisplayName("Yield mode: closing active landing")
|
||||
@Test
|
||||
fun closeActiveLandingTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val nativeCoinTitle = "Ethereum"
|
||||
val exitTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_exit)
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldActiveState = "Active"
|
||||
val yieldExitedState = "Exited"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Disable Yield Mode' button") {
|
||||
onYieldSupplyActiveScreen { stopEarningButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Confirm' button to confirm exit") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStopEarningScreen {
|
||||
confirmButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
confirmButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldExitedState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldExitedState)
|
||||
}
|
||||
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click 'Back' button to return to 'Main Screen'") {
|
||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on native coin with name: '$nativeCoinTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(nativeCoinTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode disabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(exitTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -313,6 +313,16 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="onboard-virtual-account"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="news"
|
||||
android:scheme="tangem" />
|
||||
|
|
@ -337,6 +347,16 @@
|
|||
android:host="yield"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="campaigns"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f983c6defd0b2240eb6f134aefe30f7e93696795
|
||||
Subproject commit a51f37e5f339fd83357443f4a083f7af0155fcdb
|
||||
|
|
@ -28,6 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
||||
|
|
@ -367,6 +368,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
super.onNewIntent(intent)
|
||||
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
|
||||
|
||||
// Warm start: let the AppsFlyer SDK resolve a OneLink delivered while the app is already running.
|
||||
AppsFlyerLib.getInstance().performOnDeepLinking(intent, this)
|
||||
|
||||
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
|
||||
if (isFromPush) {
|
||||
analyticsEventsHandler.send(Push.PushNotificationOpened())
|
||||
|
|
|
|||
|
|
@ -13,17 +13,9 @@ class AppsFlyerDeepLinkListener @Inject constructor(
|
|||
|
||||
override fun onDeepLinking(p0: DeepLinkResult) {
|
||||
when (p0.status) {
|
||||
DeepLinkResult.Status.FOUND -> {
|
||||
referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
}
|
||||
DeepLinkResult.Status.NOT_FOUND -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.i("No deep link found")
|
||||
}
|
||||
DeepLinkResult.Status.ERROR -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
DeepLinkResult.Status.FOUND -> referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
DeepLinkResult.Status.NOT_FOUND -> TangemLogger.i("No deep link found")
|
||||
DeepLinkResult.Status.ERROR -> TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Reactively routes AppsFlyer deep links persisted by [AppsFlyerReferralParamsHandler].
|
||||
*
|
||||
* To add a deep link: add it to [AppsFlyerDeeplink] and a branch in [onDeeplinkPending] (the `when` is exhaustive).
|
||||
*/
|
||||
@Singleton
|
||||
class AppsFlyerDeeplinkRouter @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val appRouter: AppRouter,
|
||||
) {
|
||||
|
||||
fun observe(scope: CoroutineScope, currentRoute: Flow<AppRoute?>) {
|
||||
combine(
|
||||
appsFlyerStore.observeNavigationDeeplink(),
|
||||
currentRoute.distinctUntilChanged(),
|
||||
) { deepLinkValue, route ->
|
||||
if (deepLinkValue != null && route != null) deepLinkValue to route else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { (deepLinkValue, route) -> onDeeplinkPending(deepLinkValue, route) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private suspend fun onDeeplinkPending(deepLinkValue: String, currentRoute: AppRoute) {
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding -> routeTangemPayOnboarding(currentRoute)
|
||||
AppsFlyerDeeplink.Referral -> routeReferral(currentRoute)
|
||||
null -> TangemLogger.i("Ignoring unknown AppsFlyer deep link value: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun routeTangemPayOnboarding(currentRoute: AppRoute) {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
if (!isEnabled) return
|
||||
|
||||
// Not on an idle entry screen yet: keep the deep link pending, re-evaluate on next route change.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to Tangem Pay onboarding")
|
||||
// Authorized: push onto the wallet screen so Back returns to it.
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.MobileOnboardingDeeplink))
|
||||
} else {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to hot wallet onboarding")
|
||||
// Not authorized: open onboarding as the root, skipping Home/stories (original cold-start flow).
|
||||
appRouter.replaceAll(AppRoute.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
// One-shot: consume the deep link once routed so the user isn't forced back here on relaunch.
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
}
|
||||
|
||||
private suspend fun routeReferral(currentRoute: AppRoute) {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
if (!isEnabled) return
|
||||
|
||||
// Referral targets fresh installs: from an idle entry screen, go straight to hot wallet creation
|
||||
// (skips stories). The deep link is NOT cleared here — it stays as referral attribution (read by
|
||||
// IsReferralInstallUseCase, cleared on wallet creation); replaceAll keeps it off the back stack.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) return
|
||||
|
||||
TangemLogger.i("[Referral] Routing AppsFlyer referral deep link to hot wallet creation")
|
||||
appRouter.replaceAll(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet))
|
||||
}
|
||||
}
|
||||
|
||||
// Route only from idle entry screens so an in-progress flow (scan, KYC, onboarding…) isn't interrupted.
|
||||
internal fun isIdleEntryPoint(currentRoute: AppRoute?): Boolean =
|
||||
currentRoute is AppRoute.Home || currentRoute is AppRoute.Stories || currentRoute is AppRoute.Wallet
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -19,8 +18,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val deepLinkDeferred = CompletableDeferred<String?>()
|
||||
|
||||
fun handle(params: Map<String?, Any?>) {
|
||||
handle(
|
||||
deepLinkValue = params[DEEP_LINK_VALUE] as? String,
|
||||
|
|
@ -35,40 +32,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
|
||||
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
|
||||
)
|
||||
deepLinkDeferred.complete(deepLink.deepLinkValue)
|
||||
}
|
||||
|
||||
fun handleNoDeeplink() {
|
||||
deepLinkDeferred.complete(null)
|
||||
}
|
||||
|
||||
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
|
||||
appsFlyerStore.getDeeplink(deeplinkSource)?.let { return it }
|
||||
|
||||
val expectedValue = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
AppsFlyerDeeplinkSource.Referral -> REFERRAL_DEEP_LINK_VALUE
|
||||
}
|
||||
val resolvedValue = deepLinkDeferred.await().takeIf { it == expectedValue }
|
||||
|
||||
// The deep link may have been persisted to the store while we were awaiting (e.g. from
|
||||
// conversion-data handling, which stores the deep link but doesn't complete the deferred).
|
||||
return resolvedValue ?: appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
}
|
||||
|
||||
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
|
||||
TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue")
|
||||
when (deepLinkValue) {
|
||||
REFERRAL_DEEP_LINK_VALUE -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE -> handleTangemPayHotWalletOnboarding(deepLinkValue)
|
||||
else -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTangemPayHotWalletOnboarding(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, deepLinkValue)
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link stored")
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.Referral -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding ->
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.TangemPayMobileOnboarding.deepLinkValue)
|
||||
null -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +48,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
@Suppress("NullableToStringCall")
|
||||
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
|
||||
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, REFERRAL_DEEP_LINK_VALUE)
|
||||
}
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.Referral.deepLinkValue)
|
||||
|
||||
if (!isValidParam(deepLinkSub1)) {
|
||||
TangemLogger.e("Deeplink conversion data is invalid")
|
||||
|
|
@ -97,6 +67,13 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
return value != null && value.isNotBlank() && !value.equals("null", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun storeNavigationDeeplink(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeNavigationDeeplink(deepLinkValue)
|
||||
TangemLogger.i("AppsFlyer navigation deep link stored: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeConversionData(refcode: String, campaign: String?) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
|
|
@ -107,9 +84,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
|
||||
const val REFERRAL_DEEP_LINK_VALUE = "referral"
|
||||
const val TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE = "tpay_mobileonboard"
|
||||
|
||||
const val DEEP_LINK_VALUE = "deep_link_value"
|
||||
const val DEEP_LINK_SUB_1 = "deep_link_sub1"
|
||||
const val DEEP_LINK_SUB_2 = "deep_link_sub2"
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
setAppId(context.packageName)
|
||||
setDebugLog(true)
|
||||
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
|
||||
init(apiKey, tangemAFConversionListener, context)
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
setOneLinkCustomDomain(BRANDED_ONELINK_DOMAIN)
|
||||
|
||||
TangemLogger.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
|
|
@ -100,4 +100,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
interface Factory {
|
||||
fun create(apiKey: String): AppsFlyerClient
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Branded AppsFlyer OneLink domain (matches the join.tangem.com App Link filter in AndroidManifest).
|
||||
const val BRANDED_ONELINK_DOMAIN = "join.tangem.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -34,18 +34,11 @@ class CustomerIoAnalyticsHandler(
|
|||
class Builder : AnalyticsHandlerBuilder {
|
||||
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
|
||||
val cdpApiKey = data.config.customerIoCdpApiKey
|
||||
return if (data.logConfig.isCustomerIoLogEnabled) {
|
||||
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
|
||||
} else if (!cdpApiKey.isNullOrBlank()) {
|
||||
CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(
|
||||
application = data.application,
|
||||
cdpApiKey = cdpApiKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (cdpApiKey.isNullOrBlank()) return null
|
||||
|
||||
return CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(application = data.application, cdpApiKey = cdpApiKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.tap.common.analytics.handlers.customerio
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Log client for Customer.io (used in debug mode).
|
||||
*
|
||||
* Logs all operations to Timber instead of sending them to Customer.io.
|
||||
*/
|
||||
internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
|
||||
|
||||
private var userId: String? = null
|
||||
|
||||
override fun setUserId(userId: String) {
|
||||
this.userId = userId
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
|
||||
}
|
||||
|
||||
override fun clearUserId() {
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
|
||||
this.userId = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
||||
internal class DefaultAppInfoProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppInfoProvider {
|
||||
override val platform: String
|
||||
get() = "Android"
|
||||
override val device: String
|
||||
|
|
@ -18,6 +22,8 @@ internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
|||
get() = Build.VERSION.SDK_INT
|
||||
override val language: String
|
||||
get() = Locale.getDefault().toLanguageTag()
|
||||
override val deviceScale: Float
|
||||
get() = context.resources.displayMetrics.density
|
||||
override val timezone: String
|
||||
get() = TimeZone.getDefault().id
|
||||
override val appVersion: String = BuildConfig.VERSION_NAME
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import androidx.datastore.core.DataStore
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.model.PendingOfframp
|
||||
import com.tangem.domain.offramp.model.PendingOfframp.Companion.EXPIRY_MS
|
||||
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.data.converter.PendingOfframpEntryConverter
|
||||
import com.tangem.tap.data.model.PendingOfframpEntry
|
||||
import com.tangem.tap.network.exchangeServices.SellService
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Default implementation of [OfframpRepository].
|
||||
|
|
@ -27,7 +28,7 @@ internal class DefaultOfframpRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : OfframpRepository {
|
||||
|
||||
private val pendingOfframpConverter = PendingOfframpEntryConverter()
|
||||
private val converter = PendingOfframpEntryConverter()
|
||||
|
||||
override fun getOfframpUrl(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -71,13 +72,17 @@ internal class DefaultOfframpRepository(
|
|||
entry.requestId == requestId &&
|
||||
entry.userWalletId == userWalletId.stringValue &&
|
||||
entry.currencyId == currencyId &&
|
||||
now - entry.createdAt < EXPIRY_MS
|
||||
!entry.isExpired(now)
|
||||
}
|
||||
// Keep the matched record so the same redirect can be followed again until it expires; only prune the
|
||||
// expired ones. The record is dropped naturally once it ages past EXPIRY_MS.
|
||||
stored.filterNotExpired(now)
|
||||
}
|
||||
matched?.let(pendingOfframpConverter::convert)
|
||||
matched?.let(converter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getAllStoredOfframps(): List<PendingOfframp> = withContext(dispatchers.io) {
|
||||
pendingOfframpStore.data.first().map(converter::convert)
|
||||
}
|
||||
|
||||
// Returns the same instance when nothing is expired, so DataStore.updateData sees an unchanged value and skips
|
||||
|
|
@ -85,7 +90,5 @@ internal class DefaultOfframpRepository(
|
|||
private fun List<PendingOfframpEntry>.filterNotExpired(now: Long): List<PendingOfframpEntry> =
|
||||
if (none { now - it.createdAt >= EXPIRY_MS }) this else filter { now - it.createdAt < EXPIRY_MS }
|
||||
|
||||
private companion object {
|
||||
val EXPIRY_MS: Long = TimeUnit.HOURS.toMillis(1)
|
||||
}
|
||||
private fun PendingOfframpEntry.isExpired(now: Long): Boolean = converter.convert(this).isExpired(now)
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.data.pay.entity.WithdrawStoreData
|
||||
import com.tangem.data.pay.util.WithdrawStateConverter
|
||||
import com.tangem.data.pay.util.WithdrawStoreDataConverter
|
||||
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.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
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.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val AUTH_TOKENS_DEFAULT_KEY = "tangem_pay_default_key"
|
||||
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@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 {
|
||||
AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "tangem_pay_storage",
|
||||
)
|
||||
}
|
||||
|
||||
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
|
||||
private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() }
|
||||
private val withdrawStateConverter by lazy { WithdrawStateConverter() }
|
||||
|
||||
private val listType by lazy {
|
||||
Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
|
||||
}
|
||||
private val mapType by lazy {
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, listType)
|
||||
}
|
||||
private val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> by lazy {
|
||||
appPreferencesStore.moshi.adapter(mapType)
|
||||
}
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
appPreferencesStore
|
||||
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
|
||||
)
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
createAuthTokensKey(customerWalletAddress),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
|
||||
authTokens?.let { tokens ->
|
||||
if (tokens.idempotencyKey == null) {
|
||||
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
|
||||
storeAuthTokens(customerWalletAddress, newAuthTokens)
|
||||
newAuthTokens
|
||||
} else {
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAuthTokens(customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
|
||||
}
|
||||
|
||||
override suspend fun clearOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
|
||||
isPaeraCustomer,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
|
||||
}
|
||||
|
||||
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val newItem = withdrawStoreDataConverter.convert(data)
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size + 1) {
|
||||
for (item in currentList) { if (item.orderId != newItem.orderId) add(item) }
|
||||
add(newItem)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] =
|
||||
adapter.toJson(currentMap + (walletKey to updatedList))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
|
||||
val orders = appPreferencesStore.getObjectMapSync<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
|
||||
val map = appPreferencesStore.data.firstOrNull()
|
||||
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
|
||||
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.minus(createWithdrawOrderIdKey(userWalletId))
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size) {
|
||||
for (item in currentList) {
|
||||
if (item.orderId != orderId) add(item)
|
||||
}
|
||||
}
|
||||
val updatedMap = if (updatedList.isEmpty()) {
|
||||
currentMap - walletKey
|
||||
} else {
|
||||
currentMap + (walletKey to updatedList)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
value = eligibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Set<String> {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
default = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true)
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||
val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId)
|
||||
return appPreferencesStore.getSyncOrNull(key) == true
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
|
||||
// Clear the withdraw order hints together with the rest of the cache.
|
||||
deleteActiveWithdrawOrder(userWalletId)
|
||||
clearWithdrawOrders(userWalletId)
|
||||
}
|
||||
|
||||
private suspend fun clearWithdrawOrders(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val updatedMap = currentMap - walletKey
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
|
||||
|
||||
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
|
||||
}
|
||||
|
|
@ -10,8 +10,11 @@ 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.TangemPayGenerateVirtualAccountAddressTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import dagger.Lazy
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,9 +34,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardScanHandler: VisaCardScanHandler,
|
||||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
tangemPayVirtualAccountTaskFactory: TangemPayGenerateVirtualAccountAddressTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
cardRepository: CardRepository,
|
||||
walletRegistrationLauncher: Lazy<WalletRegistrationLauncher>,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -44,9 +49,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||
tangemPayVirtualAccountTaskFactory = tangemPayVirtualAccountTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
cardRepository = cardRepository,
|
||||
walletRegistrationLauncher = walletRegistrationLauncher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
|||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.AppStoreOpener
|
||||
import com.tangem.core.navigation.url.DefaultAppStoreOpener
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.finisher.AndroidAppFinisher
|
||||
|
|
@ -35,6 +37,10 @@ internal interface UtilsModule {
|
|||
@Singleton
|
||||
fun bindAppInfoProvider(impl: DefaultAppInfoProvider): AppInfoProvider
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAppStoreOpener(impl: DefaultAppStoreOpener): AppStoreOpener
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.addressbook.crypto.AddressBookCipher
|
||||
import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor
|
||||
import com.tangem.domain.addressbook.interactor.SaveContactInteractor
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import com.tangem.domain.addressbook.time.DefaultIsoTimestampProvider
|
||||
import com.tangem.domain.addressbook.time.IsoTimestampProvider
|
||||
import com.tangem.domain.addressbook.usecase.ValidateContactAddressUseCase
|
||||
import com.tangem.domain.addressbook.usecase.VerifyAddressEntriesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.addressbook.usecase.CheckAddressDuplicateUseCase
|
||||
import com.tangem.domain.addressbook.usecase.DeleteContactUseCase
|
||||
import com.tangem.domain.addressbook.usecase.GetContactByIdUseCase
|
||||
import com.tangem.domain.addressbook.usecase.GetContactsUseCase
|
||||
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
|
||||
import com.tangem.domain.addressbook.validation.ContactNameValidator
|
||||
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.transaction.usecase.SignUseCase
|
||||
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -20,22 +28,84 @@ object AddressBookDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideValidateContactAddressUseCase(
|
||||
validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
): ValidateContactAddressUseCase {
|
||||
return ValidateContactAddressUseCase(
|
||||
validateWalletAddressUseCase = validateWalletAddressUseCase,
|
||||
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
|
||||
fun provideContactSignatureVerifier(
|
||||
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
): ContactSignatureVerifier {
|
||||
return ContactSignatureVerifier(
|
||||
verifyMessages = verifyMessagesUseCase,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVerifyAddressEntriesUseCase(
|
||||
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||
): VerifyAddressEntriesUseCase {
|
||||
return VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
||||
fun provideContactNameValidator(
|
||||
repository: AddressBookRepository,
|
||||
contactSignatureVerifier: ContactSignatureVerifier,
|
||||
): ContactNameValidator {
|
||||
return ContactNameValidator(
|
||||
repository = repository,
|
||||
contactSignatureVerifier = contactSignatureVerifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetContactsUseCase(repository: AddressBookRepository): GetContactsUseCase {
|
||||
return GetContactsUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetVerifiedContactsInteractor(
|
||||
getContactsUseCase: GetContactsUseCase,
|
||||
contactSignatureVerifier: ContactSignatureVerifier,
|
||||
): GetVerifiedContactsInteractor {
|
||||
return GetVerifiedContactsInteractor(
|
||||
getContacts = getContactsUseCase,
|
||||
contactSignatureVerifier = contactSignatureVerifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSaveContactInteractor(
|
||||
repository: AddressBookRepository,
|
||||
contactNameValidator: ContactNameValidator,
|
||||
signUseCase: SignUseCase,
|
||||
timestampProvider: IsoTimestampProvider,
|
||||
): SaveContactInteractor {
|
||||
return SaveContactInteractor(
|
||||
repository = repository,
|
||||
validateContactName = contactNameValidator,
|
||||
signUseCase = signUseCase,
|
||||
timestampProvider = timestampProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeleteContactUseCase(repository: AddressBookRepository): DeleteContactUseCase {
|
||||
return DeleteContactUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetContactByIdUseCase(repository: AddressBookRepository): GetContactByIdUseCase {
|
||||
return GetContactByIdUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckAddressDuplicateUseCase(repository: AddressBookRepository): CheckAddressDuplicateUseCase {
|
||||
return CheckAddressDuplicateUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncAddressBooksUseCase(repository: AddressBookRepository): SyncAddressBooksUseCase {
|
||||
return SyncAddressBooksUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ internal object ManageTokensDomainModule {
|
|||
return ValidateDerivationPathUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckDerivationPathSupportedUseCase(
|
||||
customTokensRepository: CustomTokensRepository,
|
||||
): CheckDerivationPathSupportedUseCase {
|
||||
return CheckDerivationPathSupportedUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.marketing.DismissMarketingBannerUseCase
|
||||
import com.tangem.domain.marketing.GetMarketingBannerUseCase
|
||||
import com.tangem.domain.marketing.MarketingFeatureToggles
|
||||
import com.tangem.domain.marketing.MarketingRepository
|
||||
import com.tangem.domain.marketing.WarmUpMarketingCampaignsUseCase
|
||||
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 MarketingDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetMarketingBannerUseCase(
|
||||
repository: MarketingRepository,
|
||||
featureToggles: MarketingFeatureToggles,
|
||||
): GetMarketingBannerUseCase = GetMarketingBannerUseCase(repository, featureToggles)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDismissMarketingBannerUseCase(repository: MarketingRepository): DismissMarketingBannerUseCase =
|
||||
DismissMarketingBannerUseCase(repository)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWarmUpMarketingCampaignsUseCase(
|
||||
repository: MarketingRepository,
|
||||
featureToggles: MarketingFeatureToggles,
|
||||
): WarmUpMarketingCampaignsUseCase = WarmUpMarketingCampaignsUseCase(repository, featureToggles)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||
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 PromoDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetPromoCampaignStateUseCase(repository: PromoRepository): GetPromoCampaignStateUseCase {
|
||||
return GetPromoCampaignStateUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEnrollPromoCampaignUseCase(repository: PromoRepository): EnrollPromoCampaignUseCase {
|
||||
return EnrollPromoCampaignUseCase(repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -46,4 +48,20 @@ internal object PushNotificationPreferencesDomainModule {
|
|||
): SetAllWalletPushNotificationPreferencesUseCase {
|
||||
return SetAllWalletPushNotificationPreferencesUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIsPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): IsPushNotificationFirstActivationDoneUseCase {
|
||||
return IsPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMarkPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): MarkPushNotificationFirstActivationDoneUseCase {
|
||||
return MarkPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,11 @@ import com.tangem.domain.swap.SwapErrorResolver
|
|||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.swap.usecase.*
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -19,12 +17,6 @@ import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal object SwapDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
|
||||
return GetAvailablePairsUseCase(swapRepository = swapRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSwapSupportedPairsUseCase(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
|||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.transaction.GaslessYieldRepository
|
||||
import com.tangem.domain.transaction.usecase.gasless.ResolveGaslessFeePlanUseCase
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
|
|
@ -319,30 +323,50 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetAvailableFeeTokensUseCase {
|
||||
return GetAvailableFeeTokensUseCase(
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideResolveGaslessFeePlanUseCase(
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
): ResolveGaslessFeePlanUseCase {
|
||||
return ResolveGaslessFeePlanUseCase(gaslessYieldRepository = gaslessYieldRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetFeeForGaslessUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
getFeeUseCase: GetFeeUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase: ResolveGaslessFeePlanUseCase,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetFeeForGaslessUseCase {
|
||||
return GetFeeForGaslessUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase = resolveGaslessFeePlanUseCase,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -351,15 +375,23 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase: ResolveGaslessFeePlanUseCase,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetFeeForTokenUseCase {
|
||||
return GetFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase = resolveGaslessFeePlanUseCase,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -379,6 +411,7 @@ internal object TransactionDomainModule {
|
|||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): CreateAndSendGaslessTransactionUseCase {
|
||||
return CreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
|
@ -386,6 +419,9 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
isGaslessV2Enabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -394,15 +430,21 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): EstimateFeeForTokenUseCase {
|
||||
return EstimateFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -411,12 +453,14 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForGaslessTxUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
estimateFeeUseCase: EstimateFeeUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForGaslessTxUseCase {
|
||||
return EstimateFeeForGaslessTxUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
|
|
@ -25,6 +26,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy
|
|||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
|
||||
import com.tangem.operations.attestation.CardArtworksProvider
|
||||
import com.tangem.tap.domain.DefaultUserWalletSelectedHandler
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -188,8 +190,16 @@ internal object WalletsDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDeleteWalletUseCase(userWalletsListRepository: UserWalletsListRepository): DeleteWalletUseCase {
|
||||
return DeleteWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||
fun providesDeleteWalletUseCase(
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
userWalletDataCleaners: Set<@JvmSuppressWildcards UserWalletDataCleaner>,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
): DeleteWalletUseCase {
|
||||
return DeleteWalletUseCase(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
userWalletDataCleaners = userWalletDataCleaners,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
|||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.domain.tasks.product.*
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateVirtualAccountAddressTask
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
|
||||
|
|
@ -57,8 +58,10 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
|||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.R
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
|
@ -72,9 +75,13 @@ internal class DefaultTangemSdkManager(
|
|||
private val visaCardScanHandler: VisaCardScanHandler,
|
||||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
private val tangemPayVirtualAccountTaskFactory: TangemPayGenerateVirtualAccountAddressTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
private val cardRepository: CardRepository,
|
||||
// Lazy breaks a DI cycle: the launcher -> hot wallet accessor -> LegacySettingsRepository ->
|
||||
// TangemSdkManager. It's only needed when a scan actually runs.
|
||||
private val walletRegistrationLauncher: Lazy<WalletRegistrationLauncher>,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val tangemSdk: TangemSdk
|
||||
|
|
@ -86,7 +93,7 @@ internal class DefaultTangemSdkManager(
|
|||
secureStorage = tangemSdk.secureStorage,
|
||||
)
|
||||
}
|
||||
override val needEnrollBiometrics: Boolean
|
||||
override val isEnrollBiometricsNeeded: Boolean
|
||||
get() {
|
||||
val isNeedEnrollBiometrics = tangemSdk.authenticationManager.needEnrollBiometrics
|
||||
if (isNeedEnrollBiometrics) {
|
||||
|
|
@ -102,7 +109,7 @@ internal class DefaultTangemSdkManager(
|
|||
|
||||
override val canUseBiometry: Boolean
|
||||
get() {
|
||||
val isCanUseBiometry = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
|
||||
val isCanUseBiometry = tangemSdk.authenticationManager.canAuthenticate || isEnrollBiometricsNeeded
|
||||
if (!isCanUseBiometry) {
|
||||
analyticsErrorHandler.sendErrorEvent(
|
||||
AnalyticsEvent(
|
||||
|
|
@ -124,7 +131,7 @@ internal class DefaultTangemSdkManager(
|
|||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return needEnrollBiometrics
|
||||
return isEnrollBiometricsNeeded
|
||||
}
|
||||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean {
|
||||
|
|
@ -145,10 +152,11 @@ internal class DefaultTangemSdkManager(
|
|||
card = null,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
sessionCoroutineScope = this,
|
||||
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
cardRepository = cardRepository,
|
||||
walletRegistrationLauncher = walletRegistrationLauncher.get(),
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
|
|
@ -531,6 +539,24 @@ internal class DefaultTangemSdkManager(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun tangemPayProduceVirtualAccountData(
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
): Either<Throwable, VirtualAccountActivationData> {
|
||||
return coroutineScope {
|
||||
val result = runTaskAsyncReturnOnMain(
|
||||
runnable = tangemPayVirtualAccountTaskFactory.create(coroutineScope = this),
|
||||
cardId = null,
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
)
|
||||
|
||||
return@coroutineScope when (result) {
|
||||
is CompletionResult.Failure<*> -> result.error.left()
|
||||
is CompletionResult.Success<VirtualAccountActivationData> -> result.data.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(
|
||||
hash: String,
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.visa.model.VirtualAccountActivationData
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
|
|
@ -46,7 +47,7 @@ class MockTangemSdkManager(
|
|||
|
||||
override val canUseBiometry: Boolean = false
|
||||
|
||||
override val needEnrollBiometrics: Boolean = false
|
||||
override val isEnrollBiometricsNeeded: Boolean = false
|
||||
|
||||
override val keystoreManager = DummyKeystoreManager()
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ class MockTangemSdkManager(
|
|||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean = canUseBiometry
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = needEnrollBiometrics
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = isEnrollBiometricsNeeded
|
||||
|
||||
override suspend fun scanProduct(
|
||||
cardId: String?,
|
||||
|
|
@ -241,6 +242,12 @@ class MockTangemSdkManager(
|
|||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun tangemPayProduceVirtualAccountData(
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
): Either<Throwable, VirtualAccountActivationData> {
|
||||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(
|
||||
hash: String,
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ object MockProvider {
|
|||
MockOption("Shiba (No Backup, No Wallets)") { ShibaNoBackupNoWalletsMockContent },
|
||||
MockOption("Ed25519 Curve") { EdCurveMockContent },
|
||||
MockOption("Secp256k1 Curve") { Secpk1CurveMockContent },
|
||||
MockOption("Wallet 2 (No ed25519_slip0010)") { Wallet2NoEd25519Slip0010MockContent },
|
||||
MockOption("Wallet 1 (Legacy derivation)") { Wallet1LegacyDerivationMockContent },
|
||||
MockOption("Firmware 4.51") { Firmware451MockContent },
|
||||
MockOption("Backup Wallet") { BackupWalletMockContent },
|
||||
MockOption("Dev Wallet") { DevWalletMockContent },
|
||||
MockOption("Firmware 4.12") { Firmware412MockContent },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Firmware 4.51: HD-capable (>= 4.39) but below SolanaTokensAvailable (4.52), so Solana tokens are firmware-limited.
|
||||
object Firmware451MockContent : MockContent by WalletMockContent {
|
||||
|
||||
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(
|
||||
firmwareVersion = WalletMockContent.cardDto.firmwareVersion.copy(minor = 51),
|
||||
)
|
||||
|
||||
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Wallet1 on a first-batch id (AC01) — resolves to the V1 (legacy) derivation style.
|
||||
object Wallet1LegacyDerivationMockContent : MockContent by WalletMockContent {
|
||||
|
||||
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(batchId = "AC01")
|
||||
|
||||
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Wallet2 without its ed25519_slip0010 wallet, so adding Solana warns UnsupportedCurve (no wallet for its curve).
|
||||
object Wallet2NoEd25519Slip0010MockContent : MockContent by Wallet2WithSeedPhraseMockContent {
|
||||
|
||||
override val cardDto: CardDTO = Wallet2WithSeedPhraseMockContent.cardDto.copy(
|
||||
wallets = Wallet2WithSeedPhraseMockContent.cardDto.wallets.filterNot {
|
||||
it.curve == EllipticCurve.Ed25519Slip0010
|
||||
},
|
||||
)
|
||||
|
||||
override val scanResponse: ScanResponse = Wallet2WithSeedPhraseMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -160,6 +160,10 @@ 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/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),
|
||||
),
|
||||
),
|
||||
extendedPublicKey = ExtendedPublicKey(
|
||||
publicKey = secp256k1WalletPublicKey,
|
||||
|
|
@ -254,6 +258,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/84'/2'/0'/0/0") to ExtendedPublicKey( // ltc (reuses valid btc key)
|
||||
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
|
||||
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
|
||||
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),
|
||||
|
|
@ -261,6 +272,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/550'/0'/0/0") to ExtendedPublicKey( // xdc (EVM, reuses valid eth key)
|
||||
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),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/60'/0'/0/1") to ExtendedPublicKey( // eth (account 2)
|
||||
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),
|
||||
|
|
@ -486,6 +504,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/3030'/0'/0'/0'") to ExtendedPublicKey( // Hedera (address resolves via network)
|
||||
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
|
||||
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ import com.tangem.operations.files.ReadFilesTask
|
|||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -42,10 +45,11 @@ import kotlinx.coroutines.launch
|
|||
internal class ScanProductTask(
|
||||
private val card: Card?,
|
||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||
private val visaCoroutineScope: CoroutineScope?,
|
||||
private val sessionCoroutineScope: CoroutineScope?,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||
private val shouldCheckIsAlreadyActivated: Boolean,
|
||||
private val cardRepository: CardRepository,
|
||||
private val walletRegistrationLauncher: WalletRegistrationLauncher? = null,
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ internal class ScanProductTask(
|
|||
val processorScanResponseWithNewCard = processorResult.data.copy(
|
||||
card = CardDTO(scanTaskResult.data),
|
||||
)
|
||||
callback(CompletionResult.Success(processorScanResponseWithNewCard))
|
||||
registerColdWalletThenComplete(session, processorScanResponseWithNewCard, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
|
||||
}
|
||||
|
|
@ -107,6 +111,38 @@ internal class ScanProductTask(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort COLD wallet registration with the Auth Service while the session is still open
|
||||
* (the card is tapped for `AttestWalletKeyTask`); the network POST is deferred by the launcher.
|
||||
* The in-session part (nonce request + attestation) runs before the scan completes, so it can
|
||||
* extend the scan slightly — but it never *fails* the scan: on any error the scan still
|
||||
* completes successfully.
|
||||
*/
|
||||
private fun registerColdWalletThenComplete(
|
||||
session: CardSession,
|
||||
scanResponse: ScanResponse,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
val scope = sessionCoroutineScope
|
||||
val launcher = walletRegistrationLauncher
|
||||
if (scope == null || launcher == null) {
|
||||
TangemLogger.i("Skipping cold wallet registration: coroutine scope or launcher unavailable")
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
runSuspendCatching { launcher.registerColdInSession(session, scanResponse) }
|
||||
.onFailure { TangemLogger.e("Cold wallet registration failed", it) }
|
||||
} finally {
|
||||
// Always complete the scan, even if the registration coroutine is cancelled
|
||||
// (runSuspendCatching rethrows CancellationException) — the scan never depends on
|
||||
// the registration outcome.
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun preflightReadMode(): PreflightReadMode {
|
||||
return if (shouldCheckIsAlreadyActivated) {
|
||||
PreflightReadMode.FullCardReadWithAccessCodeCheck
|
||||
|
|
@ -138,12 +174,12 @@ internal class ScanProductTask(
|
|||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope ?: run {
|
||||
sessionCoroutineScope ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope.launch {
|
||||
sessionCoroutineScope.launch {
|
||||
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
|
||||
is CompletionResult.Success -> {
|
||||
scanWalletProcessor.proceed(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.CardWallet
|
||||
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.common.extensions.toMapKey
|
||||
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.error.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VirtualAccountActivationData
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Derives the Virtual Account key ([VisaUtilities.virtualAccountDerivationPath]) on the card and
|
||||
* generates its deposit address. The derived key is returned (keyed by the seed wallet public key)
|
||||
* so the caller can persist it via `DerivationsRepository.storeDerivedKeys` — no second tap needed.
|
||||
*/
|
||||
class TangemPayGenerateVirtualAccountAddressTask @AssistedInject constructor(
|
||||
@Assisted private val coroutineScope: CoroutineScope,
|
||||
) : CardSessionRunnable<VirtualAccountActivationData> {
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<VirtualAccountActivationData>) {
|
||||
coroutineScope.launch {
|
||||
callback(runSuspend(session = session))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runSuspend(session: CardSession): CompletionResult<VirtualAccountActivationData> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve }
|
||||
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
|
||||
|
||||
val extendedPublicKey = when (val derivationResult = runDerivationTask(session, wallet)) {
|
||||
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
|
||||
is CompletionResult.Success<ExtendedPublicKey> -> derivationResult.data
|
||||
}
|
||||
|
||||
val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey = extendedPublicKey)
|
||||
|
||||
val derivedKeys = mapOf(
|
||||
wallet.publicKey.toMapKey() to ExtendedPublicKeysMap(
|
||||
mapOf(VisaUtilities.virtualAccountDerivationPath to extendedPublicKey),
|
||||
),
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
data = VirtualAccountActivationData(address = address, derivedKeys = derivedKeys),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun runDerivationTask(
|
||||
session: CardSession,
|
||||
wallet: CardWallet,
|
||||
): CompletionResult<ExtendedPublicKey> {
|
||||
val deferred = CompletableDeferred<CompletionResult<ExtendedPublicKey>>()
|
||||
val derivationTask = DeriveWalletPublicKeyTask(
|
||||
walletPublicKey = wallet.publicKey,
|
||||
derivationPath = VisaUtilities.virtualAccountDerivationPath,
|
||||
)
|
||||
|
||||
derivationTask.run(session = session, callback = deferred::complete)
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): TangemPayGenerateVirtualAccountAddressTask
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ class FinalizeTwinTask(
|
|||
ScanProductTask(
|
||||
card = readResult.data,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
sessionCoroutineScope = null,
|
||||
shouldCheckIsAlreadyActivated = false,
|
||||
onboardingV2FeatureToggles = null,
|
||||
cardRepository = cardRepository,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.userWalletList.di
|
||||
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.Multibinds
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface UserWalletDataCleanerModule {
|
||||
|
||||
@Multibinds
|
||||
fun userWalletDataCleaners(): Set<UserWalletDataCleaner>
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletTransformAction
|
||||
|
|
@ -622,11 +621,12 @@ internal class DefaultUserWalletsListRepository(
|
|||
|
||||
private suspend fun onFirstWalletCreated() {
|
||||
// reset the referral attribution (set from AF deeplink) after creating a new wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
}
|
||||
|
||||
private suspend fun onAllWalletsDeleted() {
|
||||
// reset the referral attribution (set from AF deeplink) after removing the last wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
appPreferencesStore.editData { it.remove(PreferencesKeys.USEDESK_CLIENT_ID_KEY) }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue