Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-22 12:15:06 +03:00
commit 125122adb7
1006 changed files with 31913 additions and 10618 deletions

12
.gitignore vendored
View file

@ -3,12 +3,24 @@
# Built application files
/build
/buildSrc
**/build/.transforms/**
**/build/classes/**
**/build/generated/**
**/build/intermediates/**
**/build/kotlin/**
**/build/libs/**
**/build/outputs/**
**/build/tmp/**
# Local configuration file (sdk path, etc)
local.properties
# Google services
# Huawei services
app/agconnect-services.json
app/src/debug/google-services.json
app/src/internal/google-services.json
app/src/external/google-services.json

View file

@ -11,9 +11,14 @@ plugins {
alias(deps.plugins.firebase.crashlytics)
alias(deps.plugins.firebase.perf)
alias(deps.plugins.ksp)
id(deps.plugins.agconnect.get().pluginId)
id("configuration")
}
agcp {
manifest = false
}
android {
namespace = "com.tangem.wallet"
testOptions {
@ -53,12 +58,35 @@ android {
keyPassword = keystoreProperties["key_password"] as String
}
}
flavorDimensions += "services"
productFlavors {
create("google") {
dimension = "services"
buildConfigField("String", "FLAVOR_NAME", "\"google\"")
}
create("huawei") {
dimension = "services"
buildConfigField("String", "FLAVOR_NAME", "\"huawei\"")
}
}
buildTypes {
debug {
buildConfigField("String", "BUILD_TYPE", "\"debug\"")
}
release {
buildConfigField("String", "BUILD_TYPE", "\"release\"")
}
}
}
configurations.all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
exclude(group = "com.github.komputing.kethereum")
exclude(group = "com.android.tools.build", module = "gradle")
resolutionStrategy {
dependencySubstitution {
@ -121,11 +149,11 @@ dependencies {
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.models)
implementation(projects.domain.notifications.toggles)
implementation(projects.domain.swap.models)
implementation(projects.domain.swap)
implementation(projects.domain.walletManager)
implementation(projects.domain.walletManager.models)
implementation(projects.domain.yieldSupply)
implementation(projects.common)
implementation(projects.common.routing)
@ -174,6 +202,7 @@ dependencies {
implementation(projects.data.notifications)
implementation(projects.data.swap)
implementation(projects.data.walletManager)
implementation(projects.data.yieldSupply)
/** Features */
implementation(projects.features.referral.impl)
@ -229,8 +258,11 @@ dependencies {
implementation(projects.features.hotWallet.api)
implementation(projects.features.hotWallet.impl)
implementation(projects.features.kyc.api)
//TODO disable for release because of the permissions
// implementation(projects.features.kyc.impl)
debugImplementation(projects.features.kyc.impl)
internalImplementation(projects.features.kyc.impl)
mockedImplementation(projects.features.kyc.impl)
releaseImplementation(projects.features.kyc.mock)
externalImplementation(projects.features.kyc.mock)
implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api)
@ -243,8 +275,12 @@ dependencies {
implementation(projects.features.tangempay.details.impl)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.main.impl)
implementation(projects.features.tangempay.onboarding.api)
implementation(projects.features.tangempay.onboarding.impl)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.tokenRecieve.impl)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.yieldSupply.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@ -318,7 +354,7 @@ dependencies {
implementation(deps.coil.gif)
implementation(deps.coil.svg)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.appsflyer)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.compose.accompanist.appCompatTheme)
@ -379,4 +415,9 @@ dependencies {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group = "com.google.guava", module = "listenablefuture")
}
/** Huawei flavor-specific dependencies */
"huaweiImplementation"(deps.huawei.push)
"huaweiImplementation"(deps.agconnect.agcp)
"huaweiImplementation"(deps.agconnect.core)
}

View file

@ -7,6 +7,13 @@
-keep class com.google.android.gms.internal.** { *; }
-keepclasseswithmembers class com.google.firebase.FirebaseException
# huawei push kit
-ignorewarnings
-keepattributes SourceFile,LineNumberTable
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
# hedera sdk
-keep class com.hedera.hashgraph.sdk.** { *; }
-keep interface com.hedera.hashgraph.sdk.** { *; }
@ -206,3 +213,7 @@
-keep class **.R$* {
<fields>;
}
# appsflyer
-keep class com.appsflyer.** { *; }
-keep class kotlin.jvm.internal.** { *; }

View file

@ -13,12 +13,19 @@ import com.kaspersky.components.composesupport.config.addComposeSupport
import com.kaspersky.kaspresso.kaspresso.Kaspresso
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.rules.ApiEnvironmentRule
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import io.qameta.allure.kotlin.Allure
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.rules.RuleChain
@ -46,6 +53,12 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject
lateinit var promoRepository: PromoRepository
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -78,6 +91,7 @@ abstract class BaseTestCase : TestCase(
additionalBeforeSection: () -> Unit = {},
additionalAfterSection: () -> Unit = {},
) = before {
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
hiltRule.inject()
runBlocking {
appPreferencesStore.editData { mutablePreferences ->
@ -86,10 +100,12 @@ abstract class BaseTestCase : TestCase(
value = false
)
}
promoRepository.setNeverToShowWalletPromo(PromoId.Sepa)
}
apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java)
Intents.init()
setFeatureToggles()
additionalBeforeSection()
}.after {
additionalAfterSection()
@ -113,4 +129,18 @@ abstract class BaseTestCase : TestCase(
{
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth)
}
private fun setFeatureToggles() {
runBlocking {
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SEND_VIA_SWAP_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("SEND_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)
}
}
}
}

View file

@ -3,5 +3,12 @@ package com.tangem.common.constants
object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN"
const val ALLURE_LABEL_NAME = "Owner"
const val ALLURE_LABEL_VALUE = "Kaspresso"
}

View file

@ -1,11 +1,15 @@
package com.tangem.common.extensions
import androidx.test.uiautomator.By
import com.tangem.common.BaseTestCase
import com.tangem.wallet.R
import io.github.kakaocup.kakao.common.utilities.getResourceString
fun BaseTestCase.swipeUp(
startHeightRatio: Float = 0.8f,
endHeightRatio: Float = 0.03f,
steps: Int = 15
fun BaseTestCase.swipeVertical(
direction: SwipeDirection,
startHeightRatio: Float = if (direction == SwipeDirection.UP) 0.8f else 0.03f,
endHeightRatio: Float = if (direction == SwipeDirection.UP) 0.03f else 0.8f,
steps: Int = 15,
) {
device.uiDevice.swipe(
device.uiDevice.displayWidth / 2,
@ -14,4 +18,41 @@ fun BaseTestCase.swipeUp(
(device.uiDevice.displayHeight * endHeightRatio).toInt(),
steps
)
}
fun BaseTestCase.pullToRefresh() {
swipeVertical(
direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f,
endHeightRatio = 0.8f,
steps = 2000
)
}
fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
val searchBarText = device.uiDevice
.findObject(By.textContains(getResourceString(R.string.markets_search_header_title)))
val bounds = searchBarText.visibleBounds
val centerX = bounds.centerX()
val startY = bounds.centerY()
val endY = when (direction) {
SwipeDirection.UP -> 50
SwipeDirection.DOWN -> device.uiDevice.displayHeight - 100
}
device.uiDevice.swipe(centerX, startY, centerX, endY, 100)
}
fun BaseTestCase.openTheAppFromRecents() {
device.uiDevice.pressRecentApps()
val centerX = device.uiDevice.displayWidth / 2
val centerY = device.uiDevice.displayHeight / 3
device.uiDevice.click(centerX, centerY)
device.uiDevice.click(centerX, centerY)
}
enum class SwipeDirection {
UP, DOWN
}

View file

@ -0,0 +1,25 @@
package com.tangem.common.extensions
import io.github.kakaocup.compose.node.element.KNode
fun assertElementDoesNotExist(
elementProvider: () -> KNode,
elementDescription: String,
) {
try {
elementProvider().assertExists()
throw AssertionError("$elementDescription should not exist but was found")
} catch (e: AssertionError) {
val isNotFoundError = e.message?.let { message ->
message.contains("No node found") ||
message.contains("scrollable container") ||
message.contains("There are no existing nodes") ||
message.contains("There are no existing nodes for that selector")
} ?: false
if (isNotFoundError) {
return
} else {
throw e
}
}
}

View file

@ -5,4 +5,5 @@ import io.github.kakaocup.compose.node.element.KNode
fun KNode.clickWithAssertion() {
assertIsDisplayed()
performClick()
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.common.extensions
import io.github.kakaocup.kakao.common.views.KBaseView
fun <T : KBaseView<*>> T.withDialogRoot(): T {
return this.also { inRoot { isDialog() } }
}

View file

@ -0,0 +1,105 @@
package com.tangem.common.utils
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
*
*/
fun getWcUri(
network: String = "ethereum",
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
.readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа
.writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи
.callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса
.build()
val request = Request.Builder()
.url("$baseUrl/wc_uri?network=$network")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
val jsonObject = JSONObject(body)
if (jsonObject.getBoolean("success")) {
val wcUri = jsonObject.getString("wcUri")
Timber.i("Got WC URI successfully: $wcUri")
wcUri
} else {
Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error getting WC URI")
null
}
}
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Checking service health")
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/health")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
if (body.isEmpty()) {
Timber.e("Response body is empty")
return null
}
val jsonObject = JSONObject(body)
val status = jsonObject.optString("status", "")
if (status.isNotEmpty()) {
Timber.i("Got status successfully: $status")
status
} else {
Timber.e("Status field is missing or empty")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error checking health")
null
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openMainScreen(
productType: ProductType? = null,
mockContent: MockContent? = null,
alreadyActivatedDialogIsShown : Boolean = false
) {
if (productType != null) {
MockProvider.setMocks(productType)
}
if (mockContent != null) {
MockProvider.setMocks(mockContent)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
}
if (alreadyActivatedDialogIsShown) {
step("Click on 'This is my wallet' button") {
composeTestRule.waitForIdle()
AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() }
}
}
step("Assert 'Main' screen is displayed") {
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Click on 'Market Tooltip' screen") {
onMarketsTooltipScreen { contentContainer.clickWithAssertion() }
}
}
fun BaseTestCase.synchronizeAddresses(balance: String) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { totalBalanceText.assertTextContains(balance) }
}
}
fun BaseTestCase.openDeviceSettingsScreen() {
step("Open wallet details") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.performClick() }
}
step("Click on 'Device settings' button") {
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.scenarios
import android.content.Intent
import android.content.Intent.*
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import io.github.kakaocup.kakao.intent.KIntent
fun openAppByDeepLink(deepLinkUri: String?) {
val deeplinkScheme = "tangem://wc?uri="
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val intent = Intent(ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
fun checkSendEMailIntentCalled() {
val expectedIntent = KIntent {
hasAction(ACTION_CHOOSER)
hasExtra(EXTRA_TITLE, "Send mail...")
hasExtraWithKey(EXTRA_INTENT)
}
expectedIntent.intended()
}

View file

@ -0,0 +1,21 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDeviceSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) {
step("Click on 'Scan card or ring' button") {
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
}
step("Assert 'Reset to Factory Settings' button title is displayed") {
onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.assertIsDisplayed() }
}
step("Assert 'Reset to Factory Settings' button subtitle is displayed") {
onDeviceSettingsScreen { resetToFactorySettingsButtonSubtitle(withBackup).assertIsDisplayed() }
}
step("Click on 'Reset to Factory Settings' button") {
onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.performClick() }
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.AlreadyUsedWalletDialogPageObject
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.cancelButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.message
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkFailedTransactionDialog() {
step("Assert failed transaction dialog is displayed") {
onFailedTransactionDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert failed transaction dialog title is displayed") {
onFailedTransactionDialog { title.assertIsDisplayed() }
}
step("Assert failed transaction dialog text is displayed") {
onFailedTransactionDialog { text.assertIsDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
onFailedTransactionDialog { cancelButton.assertIsDisplayed() }
}
step("Assert 'Support' button is displayed") {
onFailedTransactionDialog { supportButton.assertIsDisplayed() }
}
}
fun checkScanWarningDialog() {
step("Assert 'Scan warning' dialog title is displayed") {
ScanWarningDialogPageObject { warningTitle.isDisplayed() }
}
step("Assert warning dialog message is displayed") {
ScanWarningDialogPageObject { warningMessage.isDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
ScanWarningDialogPageObject { cancelButton.isDisplayed() }
}
step("Assert 'How to scan' button is displayed") {
ScanWarningDialogPageObject { howToScanButton.isDisplayed() }
}
step("Assert 'Request support' button is displayed") {
ScanWarningDialogPageObject { requestSupportButton.isDisplayed() }
}
}
fun checkAlreadyUsedWalletDialog() {
step("Assert 'Already used Wallet' dialog title is displayed") {
AlreadyUsedWalletDialogPageObject { title.isDisplayed() }
}
step("Assert dialog message is displayed") {
AlreadyUsedWalletDialogPageObject { message.isDisplayed() }
}
step("Assert 'This is my wallet' button is displayed") {
AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.isDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
AlreadyUsedWalletDialogPageObject { cancelButton.isDisplayed() }
}
step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() }
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.scenarios
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.github.kakaocup.compose.node.element.ComposeScreen
class OpenMainScreenScenario(
private val testRule: ComposeTestRule,
private val productType: ProductType? = null,
) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
if (productType != null) {
MockProvider.setMocks(productType)
}
ComposeScreen.onComposeScreen<DisclaimerPageObject>(testRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesPageObject>(testRule) {
step("Click on \"Scan\" button") {
scanButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<MainScreenPageObject>(testRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
ComposeScreen.onComposeScreen<MarketsTooltipPageObject>(testRule) {
step("Close Markets tooltip"){
contentContainer.performClick()
}
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onResetCardScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkResetCardScreen(withBackup: Boolean = false) {
step("Assert title is displayed") {
onResetCardScreen { title.assertIsDisplayed() }
}
step("Assert 'Attention' image is displayed") {
onResetCardScreen { attentionImage.assertIsDisplayed() }
}
step("Assert 'Attention' subtitle is displayed") {
onResetCardScreen { subtitle.assertIsDisplayed() }
}
step("Assert description is displayed") {
onResetCardScreen { description.assertIsDisplayed() }
}
step("Assert 'Lost wallet' checkbox is displayed") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsDisplayed() }
}
if (withBackup) {
step("Assert 'Lost password restore' checkbox is displayed") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertIsDisplayed() }
}
} else {
step("Assert 'Lost password restore' checkbox doesn't exist") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertDoesNotExist() }
}
}
}
fun BaseTestCase.checkCheckBoxLogic(withBackup: Boolean = false) {
if (withBackup) {
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Assert 'Lost wallet' checkbox is enabled") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Lost password restore' checkbox is enabled") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is enabled") {
onResetCardScreen { resetCardButton.assertIsEnabled() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
} else {
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Assert 'Lost wallet' checkbox is enabled") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is enabled") {
onResetCardScreen { resetCardButton.assertIsEnabled() }
}
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkWalletConnectBottomSheet() {
step("Assert 'Wallet Connect' bottom sheet title is displayed") {
onWalletConnectBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app icon is displayed") {
onWalletConnectBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app name is displayed") {
onWalletConnectBottomSheet { appName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet approve icon is displayed") {
onWalletConnectBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app URL is displayed") {
onWalletConnectBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request icon is displayed") {
onWalletConnectBottomSheet { connectionRequestIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request text is displayed") {
onWalletConnectBottomSheet { connectionRequestText.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request chevron is displayed") {
onWalletConnectBottomSheet { connectionRequestChevron.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet icon is displayed") {
onWalletConnectBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet title is displayed") {
onWalletConnectBottomSheet { walletNameTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet name is displayed") {
onWalletConnectBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks icon is displayed") {
onWalletConnectBottomSheet { networksIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks title is displayed") {
onWalletConnectBottomSheet { networksTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet right networks icons is displayed") {
onWalletConnectBottomSheet { networksIcons.assertIsDisplayed() }
}
step("'Wallet Connect' bottom sheet networks selector icon is displayed") {
onWalletConnectBottomSheet { networksSelectorIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Cancel' button is displayed") {
onWalletConnectBottomSheet { cancelButton.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Connect' button is displayed") {
onWalletConnectBottomSheet { connectButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectScreen() {
step("Assert 'Wallet Connect' title is displayed") {
onWalletConnectScreen { title.assertIsDisplayed() }
}
step("Assert 'More' button is displayed") {
onWalletConnectScreen { moreButton.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectScreen { walletName.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectScreen { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectScreen { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectScreen { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectScreen { appUrl.assertIsDisplayed() }
}
step("Assert 'New Connection' button is displayed") {
onWalletConnectScreen { newConnectionButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
step("Assert connection details title is displayed") {
onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() }
}
step("Assert date is displayed") {
onWalletConnectDetailsBottomSheet { date.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onWalletConnectDetailsBottomSheet { closeButton.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectDetailsBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectDetailsBottomSheet { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectDetailsBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectDetailsBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert wallet icon is displayed") {
onWalletConnectDetailsBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert wallet title is displayed") {
onWalletConnectDetailsBottomSheet { walletTitle.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectDetailsBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Connected networks' title is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworksTitle.assertIsDisplayed() }
}
step("Assert connected network item is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkItem.assertIsDisplayed() }
}
step("Assert connected network icon is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() }
}
step("Assert connected dApp name: '$dAppName'") {
onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) }
}
step("Assert connected network symbol is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() }
}
step("Assert 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.screens
import com.tangem.common.extensions.withDialogRoot
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.kakao.text.KTextView
object AlreadyUsedWalletDialogPageObject : BaseDialog() {
val title = KTextView {
withText(R.string.security_alert_title)
}.withDialogRoot()
val message = KTextView {
withText(R.string.wallet_been_activated_message)
}.withDialogRoot()
val cancelButton = KButton {
withText(R.string.common_cancel)
}.withDialogRoot()
val requestSupportButton = KButton {
withText(R.string.alert_button_request_support)
}.withDialogRoot()
val thisIsMyWalletButton = KButton {
withText(R.string.this_is_my_wallet_title)
}.withDialogRoot()
}

View file

@ -0,0 +1,22 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class BaseBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BaseBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val hideButton: KNode = child {
hasText(getResourceString(R.string.token_details_hide_token))
hasTestTag(BaseBottomSheetTestTags.ACTION_TITLE)
}
}
internal fun BaseTestCase.onBottomSheet(function: BaseBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,14 @@
package com.tangem.screens
import io.github.kakaocup.kakao.common.views.KBaseView
abstract class BaseDialog : KBaseView<BaseDialog>({ isRoot() }) {
init {
setDialogRoot()
}
private fun setDialogRoot() {
inRoot { isDialog() }
}
}

View file

@ -38,7 +38,7 @@ class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProv
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.common_unknown_error))
useUnmergedTree = true
}

View file

@ -0,0 +1,61 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.DeviceSettingsScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
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.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DeviceSettingsPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(DeviceSettingsScreenTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val imageBlock: KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK)
useUnmergedTree = true
}
val resetToFactorySettingsButtonTitle: KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE)
hasText(getResourceString(R.string.card_settings_reset_card_to_factory))
useUnmergedTree = true
}
val scanCardOrRingButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.scan_card_settings_button))
useUnmergedTree = true
}
fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE)
useUnmergedTree = true
if (withBackup) {
hasText(getResourceString(R.string.reset_card_with_backup_to_factory_message))
} else {
hasText(getResourceString(R.string.reset_card_without_backup_to_factory_message))
}
}
}
internal fun BaseTestCase.onDeviceSettingsScreen(function: DeviceSettingsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -4,7 +4,7 @@ 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.DialogTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
@ -14,7 +14,7 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(DialogTestTags.DIALOG_CONTAINER)
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val cancelButton: KNode = child {
@ -31,6 +31,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_confirm))
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_continue))
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =

View file

@ -0,0 +1,28 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.core.ui.R
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.kakao.text.KTextView
object FailedCardVerificationDialogPageObject : KScreen<FailedCardVerificationDialogPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val title = KTextView {
withId(R.id.alertTitle)
}
val message = KTextView {
withId(R.id.message)
}
val cancelButton = KButton {
withText(R.string.common_cancel)
}
val understandButton = KButton {
withText(R.string.common_understand)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.common.ui.R as CommonUIR
class FailedTransactionDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<FailedTransactionDialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.send_alert_transaction_failed_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))
}
val supportButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_support))
}
}
internal fun BaseTestCase.onFailedTransactionDialog(function: FailedTransactionDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,11 +3,11 @@ package com.tangem.screens
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasAnyAncestor
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.test.*
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.feature.wallet.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -35,7 +35,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
)
val synchronizeAddressesButton: KNode = child {
val screenContainer: KNode = child {
hasTestTag(MainScreenTestTags.SCREEN_CONTAINER)
}
val synchronizeAddressesButton: KNode = lazyList.child {
hasText(getResourceString(R.string.common_generate_addresses))
}
@ -44,6 +48,116 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(R.string.common_buy))
}
val walletNameText: KNode = child {
hasTestTag(MainScreenTestTags.CARD_TITLE)
useUnmergedTree = true
}
val walletImage: KNode = child {
hasTestTag(MainScreenTestTags.CARD_IMAGE)
useUnmergedTree = true
}
val marketPriceBlock: KNode = child {
hasTestTag(MarketPriceBlockTestTags.BLOCK)
useUnmergedTree = true
}
val marketPriceText: KNode = child {
hasTestTag(MarketPriceBlockTestTags.TEXT)
useUnmergedTree = true
}
val transactionsExplorerIcon: KNode = child {
hasTestTag(TransactionHistoryBlockTestTags.EXPLORER_ICON)
useUnmergedTree = true
}
val transactionsTitle: KNode = child {
hasTestTag(TransactionHistoryBlockTestTags.TITLE_TEXT)
hasText(getResourceString(R.string.common_transactions))
useUnmergedTree = true
}
fun transactionsExplorer(): KNode {
return child {
hasTestTag(TransactionHistoryBlockTestTags.EXPLORER_TEXT)
hasText(getResourceString(R.string.common_explorer))
useUnmergedTree = true
}
}
val notificationContainer: KNode = child {
hasTestTag(NotificationTestTags.CONTAINER)
useUnmergedTree = true
}
val devCardNotificationIcon: KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
val devCardNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_developer_card_title))
useUnmergedTree = true
}
val devCardNotificationMessage: KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.warning_developer_card_message))
useUnmergedTree = true
}
val seedPhraseNotificationIcon: KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_seedphrase_issue_title)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
val seedPhraseNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_seedphrase_issue_title))
useUnmergedTree = true
}
val seedPhraseNotificationMessage: KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.warning_seedphrase_issue_message))
useUnmergedTree = true
}
val totalBalanceContainer: KNode = child {
hasTestTag(MainScreenTestTags.WALLET_LIST_ITEM)
}
val totalBalanceMenuRenameWallet: KNode = child {
hasTestTag(MainScreenTestTags.TOTAL_BALANCE_MENU_ITEM)
hasText(getResourceString(R.string.common_rename))
}
val totalBalanceMenuDeleteWallet: KNode = child {
hasTestTag(MainScreenTestTags.TOTAL_BALANCE_MENU_ITEM)
hasText(getResourceString(R.string.common_delete))
}
val totalBalanceText: KNode = child {
hasParent(withTestTag(MainScreenTestTags.WALLET_BALANCE))
}
val notificationYesButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_yes))
useUnmergedTree = true
}
val notificationNoButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_no))
useUnmergedTree = true
}
/**
* Find token list item with title and address
*/
@ -58,19 +172,6 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
}
/**
* Find node with wallet balance using lazyList. This construction doesn't affect next step with lazyList.
*/
@OptIn(ExperimentalTestApi::class)
fun walletBalance(): KNode {
return lazyList.childWith<LazyListItemNode> {
hasAnyDescendant(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
}.child<KNode> {
hasTestTag(MainScreenTestTags.WALLET_BALANCE)
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun organizeTokensButton(): KNode {
return lazyList.childWith<LazyListItemNode> {
@ -81,6 +182,14 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
}
fun organizeTokensButtonWithoutLazySearch(): KNode {
return child {
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
hasText(getResourceString(R.string.organize_tokens_title))
useUnmergedTree = true
}
}
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
return lazyList.child {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
@ -101,6 +210,12 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
}
fun KNode.assertIsUnreachable() {
this {
hasAnyAncestor(withText(getResourceString(R.string.common_unreachable)))
assertIsDisplayed()
}
}
/**
* This assertion is required to properly verify the token's absence in the semantic tree.

View file

@ -0,0 +1,60 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.onramp.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(MarketsTestTags.TOKENS_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val addToPortfolioButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_add_to_portfolio))
useUnmergedTree = true
}
val mainNetworkSwitch: KNode = child<KNode> {
hasAnyDescendant(withText(MARKETS_MAIN_NETWORK_SUFFIX))
useUnmergedTree = true
}.child { hasTestTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH) }
val topBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
fun tokenWithTitle(title: String): KNode {
return lazyList.child<KNode> {
hasText(title)
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -5,8 +5,8 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.feature.wallet.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -14,10 +14,10 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasAnySibling as withAnySibling
import androidx.compose.ui.test.hasAnyChild as withAnyChild
import androidx.compose.ui.test.hasAnySibling as withAnySibling
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<OrganizeTokensPageObject>(semanticsProvider = semanticsProvider) {
@ -25,10 +25,12 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
// region TopBar
val title: KNode = child {
hasText(getResourceString(R.string.organize_tokens_title))
useUnmergedTree = true
}
private val topBarGroupButton: KNode = child {
hasTestTag(OrganizeTokensScreenTestTags.GROUP_BUTTON)
useUnmergedTree = true
}
val groupButton: KNode = topBarGroupButton.child {
@ -100,10 +102,14 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
return lazyList.child {
hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE)
useUnmergedTree = true
hasParent(withTestTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK)
.and(withAnySibling(withTestTag(TokenElementsTestTags.TOKEN_TITLE)
.and(withAnyChild(withText(tokenTitle))))
)
hasParent(
withTestTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK)
.and(
withAnySibling(
withTestTag(TokenElementsTestTags.TOKEN_TITLE)
.and(withAnyChild(withText(tokenTitle)))
)
)
)
}
}

View file

@ -0,0 +1,65 @@
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.ResetCardScreenTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class ResetCardPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ResetCardPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(ResetCardScreenTestTags.TITLE)
hasText(getResourceString(R.string.card_settings_reset_card_to_factory))
useUnmergedTree = true
}
val attentionImage: KNode = child {
hasTestTag(ResetCardScreenTestTags.ATTENTION_IMAGE)
useUnmergedTree = true
}
val subtitle: KNode = child {
hasTestTag(ResetCardScreenTestTags.SUBTITLE)
hasText(getResourceString(R.string.common_attention))
useUnmergedTree = true
}
val description: KNode = child {
hasTestTag(ResetCardScreenTestTags.DESCRIPTION)
useUnmergedTree = true
}
val lostWalletAccessCheckBox: KNode = child {
hasTestTag(ResetCardScreenTestTags.CHECKBOX)
hasAnySibling(
withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and
withText(getResourceString(R.string.reset_card_to_factory_condition_1))
)
useUnmergedTree = true
}
val lostPasswordRestoreCheckBox: KNode = child {
hasTestTag(ResetCardScreenTestTags.CHECKBOX)
hasAnySibling(
withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and
withText(getResourceString(R.string.reset_card_to_factory_condition_2))
)
useUnmergedTree = true
}
val resetCardButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.reset_card_to_factory_button_title))
}
}
internal fun BaseTestCase.onResetCardScreen(function: ResetCardPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,36 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KTextView
import io.github.kakaocup.kakao.text.KButton
object ScanWarningDialogPageObject : KScreen<ScanWarningDialogPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val warningTitle = KTextView {
withText(R.string.common_warning)
}
val warningMessage = KTextView {
withText(R.string.alert_troubleshooting_scan_card_title)
}
val tryAgainButton = KButton {
withId(R.id.try_again_button)
}
val howToScanButton = KButton {
withId(R.id.how_to_scan_button)
}
val requestSupportButton = KButton {
withId(R.id.request_support_button)
}
val cancelButton = KButton {
withId(R.id.cancel_button)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseSearchBarTestTags
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 SearchBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SearchBarPageObject>(semanticsProvider = semanticsProvider) {
val searchField: KNode = child {
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
}
}
internal fun BaseTestCase.onSearchBar(function: SearchBarPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.BaseSearchBarTestTags
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.onramp.impl.R
@ -12,9 +13,8 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectCountryPageObject>(semanticsProvider = semanticsProvider) {
@ -33,7 +33,7 @@ class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
)
val searchBar: KNode = child {
hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR)
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
useUnmergedTree = true
}

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.SendAddressScreenTestTags
import com.tangem.features.send.v2.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_next))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendAddressScreen(function: SendAddressPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_send))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,7 +3,7 @@ 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.StakingSendScreenTestTags
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
@ -12,11 +12,11 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasTestTag as withTestTag
class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendPageObject>(semanticsProvider = semanticsProvider) {
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER)
hasTestTag(SendScreenTestTags.SCREEN_CONTAINER)
}
val title: KNode = child {
@ -25,44 +25,44 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
val amountContainerTitle: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE)
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE)
useUnmergedTree = true
}
val amountContainerText: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT)
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT)
useUnmergedTree = true
}
val amountInputTextField: KNode = child {
hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD)
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT)
hasTestTag(SendScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val currencyButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON))
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.CURRENCY_ICON))
useUnmergedTree = true
}
val fiatButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON))
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.FIAT_ICON))
useUnmergedTree = true
}
val maxButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.MAX_BUTTON)
hasTestTag(SendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true
}
val previousButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON)
hasTestTag(SendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
@ -74,5 +74,5 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) =
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,6 +3,7 @@ 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.BaseAmountBlockTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
@ -11,8 +12,8 @@ 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
class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendDetailsPageObject>(semanticsProvider = semanticsProvider) {
class StakingConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
@ -20,12 +21,12 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP
}
val primaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT)
hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT)
hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
@ -47,5 +48,5 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP
}
internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) =
internal fun BaseTestCase.onStakingConfirmScreen(function: StakingConfirmPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -57,7 +57,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
hasTestTag(NotificationTestTags.MESSAGE)
useUnmergedTree = true
}

View file

@ -4,16 +4,18 @@ import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.common.utils.LazyListItemNode
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.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.tokendetails.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
@ -111,6 +113,49 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasText(getResourceString(R.string.common_buy))
}
@OptIn(ExperimentalTestApi::class)
val sendButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_send))
}
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
fun networkFeeNotificationTitle(feeCurrencyName: String): KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName))
useUnmergedTree = true
}
fun networkFeeNotificationMessage(
currencyName: String,
networkName: String,
feeCurrencyName: String,
feeCurrencySymbol: String,
): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(
getResourceString(
R.string.warning_send_blocked_funds_for_fee_message,
currencyName,
networkName,
currencyName,
feeCurrencyName,
feeCurrencySymbol
)
)
useUnmergedTree = true
}
fun goToBuyCurrencyButton(feeCurrencySymbol: String): KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_buy_currency, feeCurrencySymbol))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =

View file

@ -0,0 +1,108 @@
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.WalletConnectBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect))
hasTestTag(WalletConnectBottomSheetTestTags.TITLE)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val connectionRequestIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_ICON)
useUnmergedTree = true
}
val connectionRequestText: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_TEXT)
useUnmergedTree = true
}
val connectionRequestChevron: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_CHEVRON)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletNameTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val networksIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICON)
useUnmergedTree = true
}
val networksTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val networksIcons: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICONS)
useUnmergedTree = true
}
val networksSelectorIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasText(getResourceString(R.string.common_cancel))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
val connectButton: KNode = child {
hasText(getResourceString(R.string.wc_common_connect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectBottomSheet(function: WalletConnectBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,107 @@
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.WalletConnectBottomSheetTestTags
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectDetailsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectDetailsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.TITLE)
hasText(getResourceString(WalletConnectImplR.string.wc_connected_app_title))
useUnmergedTree = true
}
val date: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.DATE)
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val networksBlockTitle: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_connected_networks))
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val connectedNetworksTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val connectedNetworkItem: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ITEM)
useUnmergedTree = true
}
val connectedNetworkIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ICON)
useUnmergedTree = true
}
val connectedNetworkName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_NAME)
useUnmergedTree = true
}
val connectedNetworkSymbol: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_SYMBOL)
useUnmergedTree = true
}
val disconnectButton: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.common_disconnect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectDetailsBottomSheet(function: WalletConnectDetailsBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,59 @@
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.WalletConnectScreenTestTags
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 WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.wc_connections))
useUnmergedTree = true
}
val moreButton: KNode = child {
hasTestTag(WalletConnectScreenTestTags.MORE_BUTTON)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.WALLET_NAME)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_URL)
useUnmergedTree = true
}
val newConnectionButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wc_new_connection))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -21,7 +21,7 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup))
}
val cardSettingsButton: KNode = walletSettingsItem.child {
val deviceSettingsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.card_settings_title))
}
val referralProgramButton: KNode = walletSettingsItem.child {

View file

@ -0,0 +1,35 @@
package com.tangem.steps
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.assertElementDoesNotExist
import com.tangem.screens.onMainScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkSingleCurrencyMainScreen(cardBlockchain: String, cardTitle: String) {
step("Assert card title equal '$cardTitle'") {
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
}
step("Assert 'Transactions' block is displayed") {
onMainScreen { transactionsExplorer().assertExists() }
}
step("Assert 'Transactions' title is displayer") {
onMainScreen { transactionsTitle.assertIsDisplayed() }
}
step("Assert 'Explorer Icon' is displayed") {
onMainScreen { transactionsExplorerIcon.assertIsDisplayed() }
}
step("Assert card image is displayed") { //TODO: Придумать нормальную проверку / реализовать скриншот-тестинг
onMainScreen { walletImage.assertIsDisplayed() }
}
step("Assert 'Market Price' on single card main screen is displayed") {
onMainScreen { marketPriceBlock.assertIsDisplayed() }
}
step("Assert 'Market Price' title equals $cardBlockchain Market Price") {
onMainScreen { marketPriceText.assertTextContains("$cardBlockchain Market Price") }
}
step("Assert 'Organize tokens' button is not displayed") {
onMainScreen {
assertElementDoesNotExist({ organizeTokensButtonWithoutLazySearch() }, "Organize tokens button")
}
}
}

View file

@ -6,8 +6,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -36,13 +37,10 @@ class BuyTokenTest : BaseTestCase() {
setWireMockScenarioState(scenarioName, "Error")
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -84,13 +82,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -175,13 +170,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -250,13 +242,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -339,13 +328,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -437,13 +423,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }

View file

@ -3,7 +3,7 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.scenarios.openMainScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onReferralProgramScreen
import com.tangem.screens.onTopBar
@ -19,7 +19,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun walletWithoutBackupDetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -53,7 +55,7 @@ class DetailsTest : BaseTestCase() {
linkMoreCardsButton.assertIsDisplayed()
}
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
referralProgramButton.assertIsDisplayed()
@ -67,7 +69,9 @@ class DetailsTest : BaseTestCase() {
// @Test
fun wallet2DetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -101,7 +105,7 @@ class DetailsTest : BaseTestCase() {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
referralProgramButton.assertIsDisplayed()
@ -115,7 +119,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun noteDetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note))
step("Open 'Main Screen'") {
openMainScreen(ProductType.Note)
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -146,7 +152,7 @@ class DetailsTest : BaseTestCase() {
}
onWalletSettingsScreen {
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button does not exist") {
referralProgramButton.assertIsNotDisplayed()
@ -162,7 +168,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun validateReferralProgramScreenTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
step("Open wallet details") {
onTopBar { moreButton.clickWithAssertion() }
}

View file

@ -0,0 +1,181 @@
package com.tangem.tests
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.redux.StateDialog
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class FeedbackTest : BaseTestCase() {
@AllureId("894")
@DisplayName("Send feedback: from details")
@Test
fun sendFeedbackFromDetailsTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Contact support' button") {
onDetailsScreen { contactSupportButton.clickWithAssertion() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("893")
@DisplayName("Send feedback: failed transaction")
@Test
fun sendFeedbackFromFailedTransactionTest() {
val balance = TOTAL_BALANCE
val tokenName = "Polygon"
val recipientAddress = RECIPIENT_ADDRESS
val sendAmount = "1"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Assert input text field has value: '$sendAmount'") {
onSendScreen { amountInputTextField.assertTextContains(value = sendAmount, substring = true) }
}
step("Click 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Enter address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Click 'Send' button") {
onSendConfirmScreen {
sendButton.assertIsEnabled()
sendButton.performClick()
}
}
step("Check 'Failed transaction' dialog") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
checkFailedTransactionDialog()
}
}
step("Click on 'Support' button") {
onFailedTransactionDialog { supportButton.performClick() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("3985")
@DisplayName("Send feedback: from 'Warning' dialog after card scan")
@Test
fun sendFeedbackFromScanScreenTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
MockProvider.resetEmulateError()
}
).run {
Allure.step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Set scanning error") {
MockProvider.setEmulateError(TangemSdkError.TagLost())
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.performClick() }
}
step("Force show 'Scan warning' dialog"){
runOnUiThread {
val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN)
store.dispatch(GlobalAction.ShowDialog(scanFailsState))
}
}
step("Check 'Scan warning' dialog") {
checkScanWarningDialog()
}
step("Click on 'Request support' button") {
ScanWarningDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("3986")
@DisplayName("Send feedback: from scan already used wallet alert dialog")
@Test
fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Set mocks for Wallet2") {
MockProvider.setMocks(ProductType.Wallet2)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
}
step("Check 'Already used Wallet' dialog") {
checkAlreadyUsedWalletDialog()
}
step("Click on 'Request support' button") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
}

View file

@ -3,7 +3,8 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
@ -21,13 +22,10 @@ class HideTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -35,7 +33,7 @@ class HideTokenTest : BaseTestCase() {
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click 'More button'") {
step("Click 'More' button") {
onTokenDetailsTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Hide token' button") {

View file

@ -1,7 +1,7 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.scenarios.openMainScreen
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@ -11,7 +11,9 @@ class MainScreenTest : BaseTestCase() {
@Test
fun goToMain() {
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
}
}

View file

@ -3,9 +3,11 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.common.extensions.swipeVertical
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
import dagger.hilt.android.testing.HiltAndroidTest
@ -23,15 +25,16 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val tokenTitle = "Ethereum"
val tokenNetwork = "Ethereum network"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -55,8 +58,8 @@ class OrganizeTokensTest : BaseTestCase() {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -90,14 +93,12 @@ class OrganizeTokensTest : BaseTestCase() {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
@ -106,8 +107,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -129,10 +130,11 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
@ -142,8 +144,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -178,14 +180,12 @@ class OrganizeTokensTest : BaseTestCase() {
val polygonTitle = "Polygon"
val polExMaticTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
@ -195,8 +195,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -235,6 +235,4 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.*
import com.tangem.tap.domain.sdk.mocks.content.BackupWalletMockContent
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 ResetCardTest : BaseTestCase() {
@AllureId("3988")
@DisplayName("Reset card: reset Wallet 1.0 card without backup")
@Test
fun resetWallet1CardWithoutBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen()
}
step("Check 'Reset card' screen") {
checkResetCardScreen()
}
step("Check checkbox logic") {
checkCheckBoxLogic()
}
}
}
@AllureId("3987")
@DisplayName("Reset card: reset Wallet 1.0 card with backup")
@Test
fun resetWallet1CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = BackupWalletMockContent)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen(withBackup = true)
}
step("Check 'Reset card' screen") {
checkResetCardScreen(withBackup = true)
}
step("Check checkbox logic") {
checkCheckBoxLogic(withBackup = true)
}
}
}
@AllureId("3974")
@DisplayName("Reset card: reset Wallet 2.0 card with backup")
@Test
fun resetWallet2CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen(withBackup = true)
}
step("Check 'Reset card' screen") {
checkResetCardScreen(withBackup = true)
}
step("Check checkbox logic") {
checkCheckBoxLogic(withBackup = true)
}
}
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.tests
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.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
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 SendTest : BaseTestCase() {
@AllureId("3645")
@DisplayName("Send: check fee notification")
@Test
fun checkFeeNotificationTest() {
val currencyName = "POL (ex-MATIC)"
val feeCurrencyName = "Ethereum"
val feeCurrencySymbol = "ETH"
val balance = "$763.55"
val scenarioName = "eth_network_balance"
val scenarioState = "Empty"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: $currencyName") {
onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() }
}
step("Assert 'Insufficient $feeCurrencyName to cover network fee' notification icon is displayed") {
onTokenDetailsScreen { networkFeeNotificationIcon(feeCurrencyName).assertIsDisplayed() }
}
step("Assert 'Insufficient $feeCurrencyName to cover network fee' notification title is displayed") {
onTokenDetailsScreen { networkFeeNotificationTitle(feeCurrencyName).assertIsDisplayed() }
}
step("Assert 'Insufficient $feeCurrencyName to cover network fee' notification text is displayed") {
onTokenDetailsScreen {
networkFeeNotificationMessage(
currencyName,
feeCurrencyName,
feeCurrencyName,
feeCurrencySymbol
).assertIsDisplayed()
}
}
step("Assert 'Go to $feeCurrencySymbol' button is displayed") {
onTokenDetailsScreen { goToBuyCurrencyButton(feeCurrencySymbol).assertIsDisplayed() }
}
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
import com.tangem.steps.checkSingleCurrencyMainScreen
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 SingleCurrencyCardsScan : BaseTestCase() {
@AllureId("868")
@DisplayName("Scan: Scanning single-currency cards")
@Test
fun singleTokenNoteScanTest() {
val cardBlockchain = "DOGE"
val cardType: ProductType = ProductType.Note
setupHooks().run {
step("Open 'Main Screen' on ${cardType.name} card") {
openMainScreen(cardType)
}
step("Check 'Main' screen for ${cardType.name} $cardBlockchain card") {
checkSingleCurrencyMainScreen(cardBlockchain = cardBlockchain, cardTitle = cardType.name)
}
}
}
}

View file

@ -5,8 +5,9 @@ import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -20,7 +21,7 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingBlockTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
@ -35,13 +36,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -75,7 +76,7 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingMoreScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
val stakingAmount = "1"
@ -91,13 +92,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -151,73 +152,73 @@ class StakingTest : BaseTestCase() {
onStakingDetailsScreen { stakeMoreButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
onSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
onSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
onStakingConfirmScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
}
}
}
@ -243,13 +244,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -306,73 +307,73 @@ class StakingTest : BaseTestCase() {
onStakingDetailsScreen { stakeButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
onSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
onSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
onStakingConfirmScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
}
}
}

View file

@ -9,8 +9,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -32,13 +33,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -111,13 +109,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -149,7 +144,7 @@ class SwapTokenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@AllureId("3547")
@DisplayName("Swap: change network fee")
@Test
fun changeNetworkFeeTest() {
@ -159,13 +154,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }

View file

@ -2,8 +2,9 @@ package com.tangem.tests
import androidx.test.InstrumentationRegistry.getTargetContext
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.common.extensions.swipeVertical
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onStoriesScreen
import dagger.hilt.android.testing.HiltAndroidTest
@ -37,7 +38,8 @@ class TermsOfServiceTest : BaseTestCase() {
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
orderButton.assertIsDisplayed()
}
}
}
}
@ -67,7 +69,7 @@ class TermsOfServiceTest : BaseTestCase() {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
@ -90,7 +92,7 @@ class TermsOfServiceTest : BaseTestCase() {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
@ -98,9 +100,9 @@ class TermsOfServiceTest : BaseTestCase() {
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
orderButton.assertIsDisplayed()
}
}
}
}
}

View file

@ -0,0 +1,181 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.getWcUri
import com.tangem.scenarios.*
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class WalletConnectTest : BaseTestCase() {
@AllureId("3958")
@DisplayName("WC (React App): open session from deeplink on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("3959")
@DisplayName("WC (React App): open session from deeplink not on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionNotOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Assert 'Wallet Connect' bottom sheet is displayed") {
onWalletConnectBottomSheet { connectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("3957")
@DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSession() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Open recent apps") {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.8f)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
}

View file

@ -0,0 +1,97 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.screens.onMainScreen
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithSeedPhraseMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class WarningTest : BaseTestCase() {
@AllureId("898")
@DisplayName("Warnings: check 'Dev card' warning")
@Test
fun devCardWarningTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen(mockContent = DevWalletMockContent)
}
step("Assert 'Dev card' notification title is displayed") {
onMainScreen { devCardNotificationTitle.assertIsDisplayed() }
}
step("Assert 'Dev card' notification message is displayed") {
onMainScreen { devCardNotificationMessage.assertIsDisplayed() }
}
step("Assert 'Dev card' notification icon is displayed") {
onMainScreen { devCardNotificationIcon.assertIsDisplayed() }
}
}
}
@AllureId("3991")
@DisplayName("Warnings: check 'Dev card' warning is not displayed for release card")
@Test
fun releaseCardWarningTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Assert 'Dev card' notification title is not displayed") {
onMainScreen { devCardNotificationTitle.assertIsNotDisplayed() }
}
step("Assert 'Dev card' notification message is not displayed") {
onMainScreen { devCardNotificationMessage.assertIsNotDisplayed() }
}
step("Assert 'Dev card' notification icon is not displayed") {
onMainScreen { devCardNotificationIcon.assertIsNotDisplayed() }
}
}
}
@AllureId("227")
@DisplayName("Seed notify: check warning for wallet with seed phrase")
@Test
fun checkWarningForWalletWithSeedPhraseTest() {
val scenarioName = "seedphrase_notification"
val scenarioState = "Notified"
setupHooks(
additionalBeforeSection = {
step("Setup WireMock scenario '$scenarioName' for '$scenarioState' state") {
setWireMockScenarioState(scenarioName, scenarioState)
}
},
additionalAfterSection = {
step("Reset WireMock scenario '$scenarioName' state") {
resetWireMockScenarioState(scenarioName)
}
}
).run {
step("Open 'Main' screen") {
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true)
}
step("Assert 'Seed phrase' notification icon is displayed") {
onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() }
}
step("Assert 'Seed phrase' notification title is displayed") {
onMainScreen { seedPhraseNotificationTitle.assertIsDisplayed() }
}
step("Assert 'Seed phrase' notification message is displayed") {
onMainScreen { seedPhraseNotificationMessage.assertIsDisplayed() }
}
step("Assert notification 'Yes' button is displayed") {
onMainScreen { notificationYesButton.assertIsDisplayed() }
}
step("Assert notification 'No' button is displayed") {
onMainScreen { notificationNoButton.assertIsDisplayed() }
}
}
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.tests.balance
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
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 TotalBalanceLongTapTest : BaseTestCase() {
@Test
@AllureId("3965")
@DisplayName("Total balance: check long tap on block without biometry")
fun whenBiometryIsOffTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Long tap on total balance block") {
onMainScreen {
totalBalanceContainer.performTouchInput {
longClick()
}
}
}
step("Assert 'Rename' button is not displayed") {
onMainScreen { totalBalanceMenuRenameWallet.assertIsNotDisplayed() }
}
step("Assert 'Delete' button is not displayed") {
onMainScreen { totalBalanceMenuDeleteWallet.assertIsNotDisplayed() }
}
}
}
}

View file

@ -0,0 +1,145 @@
package com.tangem.tests.balance
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.utils.StringsSigns.DASH_SIGN
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 TotalBalanceUnavailableTest : BaseTestCase() {
@Test
@AllureId("148")
@DisplayName("Total balance: check dash sign when addresses are not derived")
fun whenDerivationsDoesNotSyncedTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Do not synchronize addresses") {
onMainScreen { synchronizeAddressesButton.assertIsDisplayed() }
}
step("Assert dash sign is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
}
}
}
@Test
@AllureId("3993")
@DisplayName("Total balance: check dash sign when network is unreachable")
fun whenNetworkIsUnreachableTest() {
val scenarioName = "eth_network_balance"
val scenarioState = "Unreachable"
val tokenTitle = "Ethereum"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {
flakySafely {
synchronizeAddressesButton.assertDoesNotExist()
}
}
}
step("Assert dash sign is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
}
step("Assert $tokenTitle is unreachable") {
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 1).assertIsUnreachable() }
}
}
}
@Test
@AllureId("3994")
@DisplayName("Total balance: check dash sign when quotes are failed")
fun whenQuotesAreFailedTest() {
val scenarioName = "quotes_api"
val scenarioState = "Error"
val tokenTitle = "Ethereum"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {
flakySafely {
synchronizeAddressesButton.assertDoesNotExist()
}
}
}
step("Assert dash sign is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
}
step("Assert $tokenTitle is unreachable") {
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 1).assertIsUnreachable() }
}
}
}
@Test
@AllureId("3995")
@DisplayName("Total balance: check dash sign when added custom token without rate")
fun whenCustomTokenWithoutRateAddedTest() {
val scenarioName = "user_tokens_api"
val scenarioState = "CustomTokenAdded"
val tokenTitle = "Myria"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(DASH_SIGN)
}
step("Assert 'Synchronize addresses' button does not exist") {
onMainScreen {
flakySafely {
synchronizeAddressesButton.assertDoesNotExist()
}
}
}
step("Assert dash sign is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
}
step("Assert $tokenTitle is unreachable") {
onMainScreen { tokenWithTitleAndPosition(tokenTitle, 4).assertIsUnreachable() }
}
}
}
}

View file

@ -0,0 +1,209 @@
package com.tangem.tests.balance
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
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 TotalBalanceUpdateTest : BaseTestCase() {
@Test
@AllureId("150")
@DisplayName("Total balance: check balance update after pull to refresh")
fun afterPullToRefreshTest() {
val scenarioName = "eth_network_balance"
val scenarioState = "Empty"
val updatedBalance = "$763.55"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh()
}
step("Assert Total balance is updated from $TOTAL_BALANCE to $updatedBalance") {
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("3997")
@DisplayName("Total balance: check balance update after token added")
fun afterAddTokenTest() {
val tokenTitle = "XRP"
val scenarioName = "quotes_api"
val scenarioState = "Ripple"
val updatedBalance = "$3,307.18"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Open 'Markets screen'") {
swipeMarketsBlock(SwipeDirection.UP)
composeTestRule.waitForIdle()
}
step("Click on $tokenTitle token") {
onMarketsScreen { tokenWithTitle(tokenTitle).clickWithAssertion() }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Click on 'Add to portfolio' button") {
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
}
step("Toggle the main network switch") {
onMarketsScreen { mainNetworkSwitch.performClick() }
}
step("Click on 'Continue' button") {
onDialog { continueButton.clickWithAssertion() }
}
step("Assert 'Continue' is not displayed") {
onDialog { continueButton.assertIsNotDisplayed() }
}
step("Go back to 'Markets: tokens list'") {
composeTestRule.waitForIdle()
onMarketsScreen { topBarBackButton.clickWithAssertion() }
}
step("Close 'Markets screen'") {
onSearchBar { searchField.assertIsDisplayed() }
swipeMarketsBlock(SwipeDirection.DOWN)
}
step("Assert $updatedBalance is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("4000")
@DisplayName("Total balance: check balance update after collapse/expand")
fun afterCollapseAndExpandTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Press 'Home' to collapse the app") {
device.uiDevice.pressHome()
}
step("Open the app from recent apps") {
openTheAppFromRecents()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
}
}
@Test
@AllureId("3998")
@DisplayName("Total balance: check balance update after hide token")
fun afterHideTokenTest() {
val tokenTitle = "Polygon"
val updatedBalance = "$3,114.34"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Long click on token with name: '$tokenTitle'") {
composeTestRule.waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Click 'Hide token' button") {
onBottomSheet { hideButton.clickWithAssertion() }
}
step("Click 'Hide' button in dialog") {
onDialog {
dialogContainer.assertIsDisplayed()
okButton.clickWithAssertion()
}
}
step("Assert $updatedBalance is displayed in total balance") {
swipeVertical(SwipeDirection.DOWN)
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("4001")
@DisplayName("Total balance: check balance update after navigation")
fun afterNavigationTest() {
val tokenTitle = "Polygon"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click 'More' button") {
onTokenDetailsTopBar { backButton.clickWithAssertion() }
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.data
package com.tangem.tap
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.data
package com.tangem.tap.di
import com.tangem.tap.data.FirebasePushNotificationsTokenProvider
import com.tangem.tap.FirebasePushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
@ -10,7 +10,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
internal interface GooglePushModule {
@Binds
@Singleton

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name="com.tangem.tap.HuaweiPushService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
</application>
</manifest>

View file

@ -0,0 +1,50 @@
package com.tangem.tap
import android.content.Context
import com.google.firebase.messaging.FirebaseMessaging
import com.huawei.agconnect.AGConnectOptionsBuilder
import com.huawei.hms.aaid.HmsInstanceId
import com.huawei.hms.common.ApiException
import com.tangem.google.GoogleServicesHelper
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
) : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(context)
return if (isGoogleServicesAvailable) {
try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
""
}
} else {
withContext(coroutineDispatcherProvider.io) {
try {
val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY)
val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE)
Timber.i("Requested token from HuaweiService: $token")
token
} catch (e: ApiException) {
Timber.i("Fetching token from HuaweiService failed cause: ${e.message}")
""
}
}
}
}
companion object {
private const val APP_ID_KEY = "client/app_id"
private const val TOKEN_REQUEST_MODE = "HCM"
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap
import android.os.Bundle
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
import com.tangem.google.GoogleServicesHelper
import com.tangem.tap.common.pushes.PushNotificationDelegate
import timber.log.Timber
class HuaweiPushService : HmsMessageService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String?, bundle: Bundle?) {
super.onNewToken(token, bundle)
Timber.i("HuaweiPushService: On new token from HuaweiService: $token")
}
override fun onTokenError(e: Exception?, bundle: Bundle?) {
super.onTokenError(e, bundle)
Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}")
}
override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message)
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
if (isGoogleServicesAvailable) return
val notification = message?.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
pushNotificationDelegate.showNotification(
dataMap = message.dataOfMap,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.urgency,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateConfig,
)
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.HuaweiPushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface HuaweiPushModule {
@Binds
@Singleton
fun bindPushNotificationsTokenProvider(impl: HuaweiPushNotificationsTokenProvider): PushNotificationsTokenProvider
}

View file

@ -34,6 +34,7 @@
<application
android:name="com.tangem.tap.TangemHiltApplication"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="false"
android:hardwareAccelerated="true"
@ -45,7 +46,7 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label, android:largeHeap">
tools:replace="android:allowBackup, android:dataExtractionRules, android:fullBackupContent, android:label, android:largeHeap">
<!--
property to make the "android:screenOrientation" work on API <37
@ -264,6 +265,16 @@
android:host="promo"
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="onboard-visa"
android:scheme="tangem" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -1,160 +1,165 @@
{
"coins" : [
"coins": [
{
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"networks" : [
"id": "matic-token",
"symbol": "MATIC",
"name": "Polygon",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x0000000000000000000000000000000000001010",
"decimalCount": 18
}
]
},
{
"id" : "kaspa",
"symbol" : "KAS",
"name" : "Kaspa",
"networks" : [
"id": "kaspa",
"symbol": "KAS",
"name": "Kaspa",
"networks": [
{
"networkId" : "kaspa/test"
"networkId": "kaspa/test"
}
]
},
{
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"networks" : [
"id": "Dai Stablecoin-DAI",
"symbol": "DAI",
"name": "Dai Stablecoin",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount": 18
}
]
},
{
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"networks" : [
"id": "Dummy ERC20-DERC20",
"symbol": "DERC20",
"name": "Dummy ERC20",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount": 18
}
]
},
{
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"networks" : [
"id": "Ether-ETH",
"symbol": "ETH",
"name": "Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount": 18
}
]
},
{
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"networks" : [
"id": "Test Token-TST",
"symbol": "TST",
"name": "Test Token",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount": 18
}
]
},
{
"id" : "tether",
"symbol" : "USDT",
"name" : "Tether USD",
"networks" : [
"id": "tether",
"symbol": "USDT",
"name": "Tether USD",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
"networkId": "ethereum/test",
"contractAddress": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount": 6
},
{
"networkId" : "tron/test",
"contractAddress" : "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount" : 6
"networkId": "binance-smart-chain/test",
"contractAddress": "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount": 6
},
{
"networkId": "binance-smart-chain/test",
"contractAddress": "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount": 18
}
]
},
{
"id" : "just-gov",
"symbol" : "JST",
"name" : "JUST GOV",
"networks" : [
"id": "just-gov",
"symbol": "JST",
"name": "JUST GOV",
"networks": [
{
"networkId" : "tron/test",
"contractAddress" : "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"networks" : [
"id": "Wrapped Ether-WETH",
"symbol": "WETH",
"name": "Wrapped Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"networks" : [
"id": "Wrapped Matic-WMATIC",
"symbol": "WMATIC",
"name": "Wrapped Matic",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount": 18
}
]
},
{
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"networks" : [
"id": "solana",
"symbol": "SOL",
"name": "Solana",
"networks": [
{
"networkId" : "solana/test"
"networkId": "solana/test"
}
]
},
@ -162,263 +167,262 @@
"id": "the-open-network",
"symbol": "TON",
"name": "Toncoin",
"networks":
[
"networks": [
{
"networkId": "the-open-network/test"
}
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"networks" : [
"id": "Tangem Coin A-TCA",
"symbol": "TCA",
"name": "Tangem Coin A",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount": 9
}
]
},
{
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"networks" : [
"id": "Tangem Coin B-TCB",
"symbol": "TCB",
"name": "Tangem Coin B",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount": 9
}
]
},
{
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"networks" : [
"id": "binancecoin",
"symbol": "BNB",
"name": "Binance",
"networks": [
{
"networkId" : "binancecoin/test"
"networkId": "binancecoin/test"
},
{
"networkId" : "binance-smart-chain/test"
"networkId": "binance-smart-chain/test"
}
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"networks" : [
"id": "Hemster - 452-HEM",
"symbol": "HEM",
"name": "Hemster - 452",
"networks": [
{
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
"networkId": "binancecoin/test",
"contractAddress": "HEM-452",
"decimalCount": 8
}
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"networks" : [
"id": "Binance-Peg BTCB Token-BTCB",
"symbol": "BTCB",
"name": "Binance-Peg BTCB Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"networks" : [
"id": "Binance-Peg BUSD Token-BUSD",
"symbol": "BUSD",
"name": "Binance-Peg BUSD Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"networks" : [
"id": "Binance-Peg USDC Token-USDC",
"symbol": "USDC",
"name": "Binance-Peg USDC Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"networks" : [
"id": "Binance-Peg XRP-XRP",
"symbol": "XRP",
"name": "Binance-Peg XRP",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount": 18
}
]
},
{
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"networks" : [
"id": "ethereum",
"symbol": "ETH",
"name": "Ethereum",
"networks": [
{
"networkId" : "ethereum/test"
"networkId": "ethereum/test"
}
]
},
{
"id" : "ethereum-classic",
"symbol" : "ETC",
"name" : "Ethereum Classic",
"networks" : [
"id": "ethereum-classic",
"symbol": "ETC",
"name": "Ethereum Classic",
"networks": [
{
"networkId" : "ethereum-classic/test"
"networkId": "ethereum-classic/test"
}
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"networks" : [
"id": "Weenus-WEENUS",
"symbol": "WEENUS",
"name": "Weenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount": 18
}
]
},
{
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"networks" : [
"id": "Xeenus-XEENUS",
"symbol": "XEENUS",
"name": "Xeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount": 18
}
]
},
{
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"networks" : [
"id": "Yeenus-YEENUS",
"symbol": "YEENUS",
"name": "Yeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
"networkId": "ethereum/test",
"contractAddress": "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount": 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"networks" : [
"id": "Zeenus-ZEENUS",
"symbol": "ZEENUS",
"name": "Zeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
"networkId": "ethereum/test",
"contractAddress": "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount": 0
}
]
},
{
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"networks" : [
"id": "avalanche-2",
"symbol": "AVAX",
"name": "Avalanche",
"networks": [
{
"networkId" : "avalanche/test"
"networkId": "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"networks" : [
"id": "The Fuji stablecoin-FUJISTABLE",
"symbol": "FUJISTABLE",
"name": "The Fuji stablecoin",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
"networkId": "avalanche/test",
"contractAddress": "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount": 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"networks" : [
"id": "To the Moon-FUJIMOON",
"symbol": "FUJIMOON",
"name": "To the Moon",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
"networkId": "avalanche/test",
"contractAddress": "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount": 9
}
]
},
{
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"networks" : [
"id": "fantom",
"symbol": "FTM",
"name": "Fantom",
"networks": [
{
"networkId" : "fantom/test"
"networkId": "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"networks" : [
"id": "Fantom USD-FUSD",
"symbol": "FUSD",
"name": "Fantom USD",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"networks" : [
"id": "Wrapped Fantom-WFTM",
"symbol": "WFTM",
"name": "Wrapped Fantom",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount": 18
}
]
},
{
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"networks" : [
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"networks": [
{
"networkId" : "bitcoin/test"
"networkId": "bitcoin/test"
}
]
},
{
"id" : "vechain",
"symbol" : "VET",
"name" : "VeChain",
"networks" : [
"id": "vechain",
"symbol": "VET",
"name": "VeChain",
"networks": [
{
"networkId" : "vechain/test"
"networkId": "vechain/test"
}
]
},
@ -435,34 +439,34 @@
]
},
{
"id" : "tron",
"symbol" : "TRX",
"name" : "Tron",
"networks" : [
{
"networkId" : "tron/test"
}
]
"id": "tron",
"symbol": "TRX",
"name": "Tron",
"networks": [
{
"networkId": "tron/test"
}
]
},
{
"id" : "algorand",
"symbol" : "ALGO",
"name" : "Algorand",
"networks" : [
{
"networkId" : "algorand/test"
}
]
"id": "algorand",
"symbol": "ALGO",
"name": "Algorand",
"networks": [
{
"networkId": "algorand/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
"name" : "Arbitrum",
"networks" : [
{
"networkId" : "arbitrum-one/test"
}
]
"id": "arbitrum-one",
"symbol": "ETH",
"name": "Arbitrum",
"networks": [
{
"networkId": "arbitrum-one/test"
}
]
},
{
"id": "stellar",
@ -485,12 +489,12 @@
]
},
{
"id" : "polkadot",
"symbol" : "DOT",
"name" : "Polkadot",
"networks" : [
"id": "polkadot",
"symbol": "DOT",
"name": "Polkadot",
"networks": [
{
"networkId" : "polkadot/test"
"networkId": "polkadot/test"
}
]
},
@ -509,8 +513,7 @@
"id": "kava",
"symbol": "KAVA",
"name": "Kava EVM",
"networks":
[
"networks": [
{
"networkId": "kava/test"
}
@ -520,8 +523,7 @@
"id": "telos",
"symbol": "TLOS",
"name": "Telos EVM",
"networks":
[
"networks": [
{
"networkId": "telos/test"
}
@ -531,8 +533,7 @@
"id": "ravencoin",
"symbol": "RVN",
"name": "Ravencoin",
"networks":
[
"networks": [
{
"networkId": "ravencoin/test"
}
@ -542,8 +543,7 @@
"id": "cosmos",
"symbol": "ATOM",
"name": "Cosmos Hub",
"networks":
[
"networks": [
{
"networkId": "cosmos/test"
}
@ -798,6 +798,30 @@
"networkId": "alephium/test"
}
]
},
{
"id": "chainlink",
"name": "Chainlink",
"symbol": "LINK",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5",
"decimalCount": 18
}
]
},
{
"id": "gho",
"name": "GHO",
"symbol": "GHO",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xc4bF5CbDaBE595361438F8c6a187bDc330539c60",
"decimalCount": 18
}
]
}
]
}

View file

@ -29,12 +29,13 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -50,7 +51,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@EntryPoint
@InstallIn(SingletonComponent::class)
@ -71,8 +71,6 @@ interface ApplicationEntryPoint {
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
@ -107,7 +105,7 @@ interface ApplicationEntryPoint {
fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getWalletMetaInfoUseCase(): GetWalletMetaInfoUseCase
fun getUrlOpener(): UrlOpener
@ -151,4 +149,6 @@ interface ApplicationEntryPoint {
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -22,6 +23,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
@Suppress("LongParameterList")
internal class LockUserWalletsTimer(
private val context: Context,
private val settingsRepository: SettingsRepository,
@ -30,6 +32,7 @@ internal class LockUserWalletsTimer(
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val coroutineScope: CoroutineScope,
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -55,6 +58,9 @@ internal class LockUserWalletsTimer(
)
if (shouldOpenWelcomeScreenOnResume) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
clearAllHotWalletContextualUnlockUseCase.invoke()
}
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
@ -120,6 +126,7 @@ internal class LockUserWalletsTimer(
start()
}
.onRight {
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}

View file

@ -26,10 +26,8 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext
@ -40,7 +38,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
@ -49,12 +47,11 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
@ -62,13 +59,9 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
@ -116,9 +109,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var scanCardUseCase: ScanCardUseCase
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@ -171,24 +161,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var deeplinkFactory: DeepLinkFactory
@Inject
internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles
@Inject
internal lateinit var urlOpener: UrlOpener
@Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher
@Inject
internal lateinit var intentProcessor: IntentProcessor
@Inject
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
@Inject
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
@Inject
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
@ -198,10 +176,13 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
@Inject
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -256,6 +237,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
if (BuildConfig.TESTER_MENU_ENABLED) {
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
}
if (intent != null) {
handleDeepLink(intent = intent, isFromOnNewIntent = false)
}
}
private fun setRootContent() {
@ -265,6 +250,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
initialStack = appRouterConfig.stack,
launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intent),
)
setContent {
@ -286,14 +272,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
hotWalletFeatureToggles = hotWalletFeatureToggles,
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
)
initIntentHandlers()
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
scanCardUseCase = scanCardUseCase,
walletConnectInteractor = walletConnectInteractor,
cardSdkConfigRepository = cardSdkConfigRepository,
),
)
@ -347,18 +331,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
dialogManager.onStart(this)
}
override fun onResume() {
super.onResume()
navigateToInitialScreenIfNeeded(intent)
}
override fun onStop() {
dialogManager.onStop()
super.onStop()
}
override fun onDestroy() {
intentProcessor.removeAll()
// workaround: kill process when activity destroy to avoid state when lock() wallets
// and navigation to unlock screen was skipped because system kills activity but not process
if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {
@ -367,14 +345,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
super.onDestroy()
}
private fun initIntentHandlers() {
intentProcessor.addHandler(onPushClickedIntentHandler)
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(walletConnectLinkIntentHandler)
}
}
private fun updateAppTheme(appThemeMode: AppThemeMode) {
val mode = when (appThemeMode) {
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
@ -400,8 +370,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
lifecycleScope.launch {
intentProcessor.handleIntent(intent = intent, isFromForeground = true)
val fromPush = intent?.extras?.containsKey(OPENED_FROM_GCM_PUSH) ?: false
if (fromPush) {
analyticsEventsHandler.send(Push.PushNotificationOpened)
}
if (intent != null) {
@ -443,131 +414,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
// TODO refactor this method to return a route instead of navigating directly
if (hotWalletFeatureToggles.isHotWalletEnabled) {
navigateToInitialScreenIfNeededNew(intentWhichStartedActivity)
return
}
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial }
val isOnInitialScreen = backStack.all { it is AppRoute.Welcome || it is AppRoute.Home }
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
when {
!isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStack.isEmpty() -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
isOnlyInitialRoute -> navigateToInitialScreen(intentWhichStartedActivity)
else -> Unit
}
}
@Deprecated("Refactor this method to return a route instead of navigating directly")
private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) {
lifecycleScope.launch {
val userWallets = userWalletsListRepository.userWalletsSync()
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWallets.isEmpty()) {
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = false,
)
} else {
if (userWallets.any { it.isLocked }) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
} else {
store.dispatchNavigationAction {
replaceAll(AppRoute.Wallet)
}
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = true,
)
}
if (intent != null) {
handleDeepLink(intent = intent, isFromOnNewIntent = false)
}
viewModel.checkForUnfinishedBackup()
}
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails)
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = true,
)
} else {
lifecycleScope.launch {
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = false,
)
}
}
if (intent != null) {
handleDeepLink(intent = intent, isFromOnNewIntent = false)
}
viewModel.checkForUnfinishedBackup()
}
private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) {
val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri()
val webLink = intent.getStringExtra(WEBLINK_KEY)
@ -636,5 +482,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
companion object {
private const val APP_THEME_LOAD_TIMEOUT = 2
private const val MOCKED_BUILD_TYPE = "mocked"
private const val OPENED_FROM_GCM_PUSH = "google.sent_time" // every bundle from FCM contains this key
}
}

View file

@ -47,7 +47,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -63,6 +63,7 @@ import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer
@ -75,9 +76,12 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
import timber.log.Timber
lateinit var store: Store<AppState>
@ -111,9 +115,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCardScanningFeatureToggles()
private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository()
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
@ -165,8 +166,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getSendFeedbackEmailUseCase()
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase
get() = entryPoint.getWalletMetaInfoUseCase()
private val urlOpener
get() = entryPoint.getUrlOpener()
@ -236,6 +237,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
// endregion
private val appScope = MainScope()
@ -289,15 +293,16 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
tangemAppLoggerInitializer.initialize()
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
@ -329,7 +334,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
)
appStateHolder.mainStore = store
walletConnect2Repository.init(
wcInitializeUseCase.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
}
@ -342,7 +348,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager,
cardScanningFeatureToggles = cardScanningFeatureToggles,
walletConnectRepository = walletConnect2Repository,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
@ -357,7 +362,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
issuersConfigStorage = issuersConfigStorage,
urlOpener = urlOpener,
shareManager = shareManager,
@ -401,6 +406,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addFilter(oneTimeEventFilter)

View file

@ -5,9 +5,10 @@ import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.SimpleOkDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
@ -59,88 +60,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
context = context,
)
is WalletConnectDialog.AddNetwork -> {
val message = context.getString(
R.string.wallet_connect_error_missing_blockchains,
) + state.dialog.networks.joinToString()
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = message,
context = context,
)
}
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_same_wcuri,
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_timeout,
context = context,
)
}
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context)
is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context)
is WalletConnectDialog.BnbTransactionDialog ->
BnbTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork -> {
val warning = if (state.dialog.networks.isNullOrEmpty()) {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks.joinToString()
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = warning,
context = context,
)
}
is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_unsupported_dapp,
context = context,
)
is WalletConnectDialog.SessionProposalDialog -> {
SessionProposalDialog.create(
sessionProposal = state.dialog.sessionProposal,
networks = state.dialog.networks,
context = context,
onApprove = state.dialog.onApprove,
onReject = state.dialog.onReject,
)
}
is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.SignTransactionsDialog -> SignTransactionsDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = state.dialog.error.message,
context = context,
)
is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,

View file

@ -1,120 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
/**
[REDACTED_AUTHOR]
*/
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Wallet Connect", event, params) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionInitiated(source: SourceType) : WalletConnect(
event = "Session Initiated",
params = mapOf(
AnalyticsParam.SOURCE to when (source) {
SourceType.QR -> "QR"
SourceType.DEEPLINK -> "DeepLink"
SourceType.CLIPBOARD -> "Clipboard"
SourceType.ETC -> "etc"
},
),
)
data object SessionFailed : WalletConnect(
event = "Session Failed",
)
class DAppConnectionRequested(
blockchainNames: List<String>,
) : WalletConnect(
event = "dApp Connection Requested",
params = mapOf(
AnalyticsParam.NETWORKS to blockchainNames.joinToString(","),
),
)
class DAppConnected(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class DAppConnectionFailed(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connection Failed",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect(
event = "dApp Disconnected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
),
)
class SignatureRequestHandled(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Handled",
params = params.toParamsMap(),
)
class SignatureRequestReceived(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Received",
params = params.toParamsMap(),
)
class SignatureRequestFailed(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Failed",
params = params.toParamsMap(),
)
data class RequestHandledParams(
val dAppName: String,
val dAppUrl: String,
val methodName: String,
val blockchain: String,
val errorCode: String? = null,
val errorDescription: String? = null,
) {
fun toParamsMap(): Map<String, String> {
val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param
val code = errorCode ?: SUCCESS_CODE
return buildMap {
put(AnalyticsParam.DAPP_NAME, dAppName)
put(AnalyticsParam.DAPP_URL, dAppUrl)
put(AnalyticsParam.METHOD_NAME, methodName)
put(AnalyticsParam.BLOCKCHAIN, blockchain)
put(AnalyticsParam.VALIDATION, validation)
put(AnalyticsParam.ERROR_CODE, code)
if (errorDescription != null) {
put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription)
}
}
}
}
enum class Validation(val param: String) {
SUCCESS("Success"),
FAIL("Fail"),
}
private companion object {
const val SUCCESS_CODE = "0"
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.tap.common.analytics.handlers.appsflyer
import android.content.Context
import com.appsflyer.AppsFlyerLib
import com.tangem.core.analytics.api.EventLogger
interface AppsFlyerAnalyticsClient : EventLogger
internal class AppsFlyerClient(
private val context: Context,
key: String,
appId: String,
) : AppsFlyerAnalyticsClient {
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
init {
appsFlyerLib.init(key, null, context)
appsFlyerLib.setAppId(appId)
appsFlyerLib.start(context)
}
override fun logEvent(event: String, params: Map<String, String>) {
appsFlyerLib.logEvent(context, event, params)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.tap.common.analytics.handlers.appsflyer
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsHandler {
override fun id(): String = ID
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}
override fun send(event: AnalyticsEvent) {
super.send(event)
}
companion object {
const val ID = "AppsFlyer"
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerApiKey, data.config.appsAppId)
data.isDebug && data.logConfig.appsflyer -> AppsFlyerLogClient(data.jsonConverter)
else -> null
}?.let { AppsFlyerAnalyticsHandler(it) }
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.common.analytics.handlers.appsflyer
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
internal class AppsFlyerLogClient(
jsonConverter: MoshiJsonConverter,
) : AppsFlyerAnalyticsClient {
private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter)
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.buildconfig
import com.tangem.utils.buildConfig.AppConfigurationProvider
import com.tangem.wallet.BuildConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class AppConfigurationProviderImpl @Inject constructor() : AppConfigurationProvider {
override fun isDebug(): Boolean = BuildConfig.BUILD_TYPE == "debug"
override fun isHuawei(): Boolean = BuildConfig.FLAVOR_NAME == "huawei"
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.common.pushes
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.wallet.R
class PushNotificationDelegate(private val context: Context) {
@Suppress("LongParameterList")
fun showNotification(
dataMap: Map<String, String>,
title: String?,
body: String?,
channelId: String,
priority: Int,
imageUrl: Uri? = null,
vibratePattern: LongArray?,
) {
val intent = Intent(context, MainActivity::class.java).apply {
dataMap.forEach { (key, value) ->
putExtra(key, value)
}
putExtra(OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(title)
.setContentText(body)
.setPriority(priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(vibratePattern)
.apply {
imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(context, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
context,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(context)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
private const val OPENED_FROM_GCM_PUSH = "google.sent_time" // every bundle from FCM contains this key
}
}

View file

@ -1,30 +1,17 @@
package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
import timber.log.Timber
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
@ -36,73 +23,18 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
val intent = Intent(applicationContext, MainActivity::class.java).apply {
message.data.forEach {
putExtra(it.key, it.value)
}
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
pushNotificationDelegate.showNotification(
dataMap = message.data,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.priority,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateTimings,
)
val notificationBuilder =
NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(notification.title)
.setContentText(notification.body)
.setPriority(message.priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(notification.vibrateTimings)
.apply {
notification.imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(applicationContext, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
applicationContext,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(applicationContext)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -5,8 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -19,7 +17,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -30,7 +27,6 @@ data class AppState(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

@ -0,0 +1,77 @@
package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.text.encodeToByteArray
private const val DEFAULT_KEY = "tangem_pay_default_key"
private const val DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY = "tangem_pay_default_customer_wallet_address_key"
@Singleton
internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : TangemPayStorage {
private val secureStorage by lazy {
AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "tangem_pay_storage",
)
}
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) =
withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)
secureStorage.store(
json.encodeToByteArray(throwOnInvalidSequence = true),
createKey(customerWalletAddress),
)
}
override suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? =
withContext(dispatcherProvider.io) {
secureStorage.get(createKey(customerWalletAddress))
?.decodeToString(throwOnInvalidSequence = true)
?.let(tokensAdapter::fromJson)
}
/**
* Store the only customer wallet address, since for the f&f user can issue only one card tied to one address
*/
override suspend fun storeCustomerWalletAddress(customerWalletAddress: String) =
withContext(dispatcherProvider.io) {
secureStorage.store(
customerWalletAddress.encodeToByteArray(throwOnInvalidSequence = true),
DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY,
)
}
override suspend fun getCustomerWalletAddress(): String? = withContext(dispatcherProvider.io) {
secureStorage.get(DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY)?.decodeToString(throwOnInvalidSequence = true)
}
override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(createKey(customerWalletAddress))
}
private fun createKey(cardId: String): String = "${DEFAULT_KEY}_$cardId"
}

View file

@ -19,6 +19,9 @@ internal class RuntimeUserWalletsStore(
override val userWallets: Flow<List<UserWallet>>
get() = userWalletsListManager.userWallets
override val userWalletsSync: List<UserWallet>
get() = userWalletsListManager.userWalletsSync
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
}

View file

@ -3,9 +3,9 @@ package com.tangem.tap.data
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -24,6 +24,9 @@ class UserWalletsStoreRepositoryProxy(
}
}
override val userWalletsSync: List<UserWallet>
get() = userWalletsListRepository.userWallets.value.orEmpty()
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListRepository.userWallets.value?.find { it.walletId == key }
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.common.buildconfig.AppConfigurationProviderImpl
import com.tangem.utils.buildConfig.AppConfigurationProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppConfigurationModule {
@Binds
@Singleton
fun bindAppConfigurationProvider(impl: AppConfigurationProviderImpl): AppConfigurationProvider
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,17 +14,4 @@ internal object IntentHandlingModule {
@Provides
@Singleton
fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler()
@Provides
@Singleton
fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler()
@Provides
@Singleton
fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler =
OnPushClickedIntentHandler(analyticsEventHandler)
@Provides
@Singleton
fun provideIntentProcessor(): IntentProcessor = IntentProcessor()
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultTangemPayStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds
@ -21,4 +23,8 @@ internal interface VisaStorageModule {
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
@Binds
@Singleton
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.*
import dagger.Module
@ -42,6 +43,12 @@ internal object AccountDomainModule {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideGetArchivedAccountsUseCase(accountsCRUDRepository: AccountsCRUDRepository): GetArchivedAccountsUseCase {
return GetArchivedAccountsUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideGetUnoccupiedAccountIndexUseCase(
@ -49,4 +56,16 @@ internal object AccountDomainModule {
): GetUnoccupiedAccountIndexUseCase {
return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideIsAccountsModeEnabledUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
accountsFeatureToggles: AccountsFeatureToggles,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
crudRepository = accountsCRUDRepository,
accountsFeatureToggles = accountsFeatureToggles,
)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
@ -18,8 +18,8 @@ internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase {
return GetCardInfoUseCase(feedbackRepository = feedbackRepository)
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetWalletMetaInfoUseCase {
return GetWalletMetaInfoUseCase(feedbackRepository = feedbackRepository)
}
@Provides

View file

@ -6,7 +6,6 @@ import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -33,29 +32,21 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesFetchNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): FetchNFTCollectionsUseCase = FetchNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@Singleton
fun providesRefreshAllNFTUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshAllNFTUseCase = RefreshAllNFTUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@ -127,16 +118,12 @@ internal object NFTDomainModule {
fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase(
walletsRepository = walletsRepository,
nftRepository = nftRepository,
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -1,11 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.*
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.repository.PushNotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Module
import dagger.Provides
@ -79,12 +76,6 @@ internal object NotificationsDomainModule {
)
}
@Provides
@Singleton
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
@Provides
@Singleton
fun provideGetNetworksAvailableForNotifications(

View file

@ -1,11 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module
import dagger.Provides
@ -27,6 +23,15 @@ internal object OnrampDomainModule {
return GetOnrampCurrenciesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampCurrencyUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampGetDefaultCurrencyUseCase {
return OnrampGetDefaultCurrencyUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCurrencyUseCase(
@ -120,6 +125,12 @@ internal object OnrampDomainModule {
return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSepaAvailableUseCase(onrampRepository: OnrampRepository): OnrampSepaAvailableUseCase {
return OnrampSepaAvailableUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampUpdateTransactionStatusUseCase(
@ -231,4 +242,34 @@ internal object OnrampDomainModule {
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
}
@Provides
@Singleton
fun provideGetOnrampAllOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
settingsRepository: SettingsRepository,
): GetOnrampAllOffersUseCase {
return GetOnrampAllOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
settingsRepository = settingsRepository,
)
}
@Provides
@Singleton
fun provideGetOnrampOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository,
settingsRepository: SettingsRepository,
): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository,
settingsRepository = settingsRepository,
)
}
}

View file

@ -15,9 +15,10 @@ internal object PromoDomainModule {
@Provides
@Singleton
fun provideShouldShowSwapPromoWalletUseCase(
promoSettingsRepository: PromoRepository,
promoRepository: PromoRepository,
settingsRepository: SettingsRepository,
): ShouldShowPromoWalletUseCase {
return ShouldShowPromoWalletUseCase(promoSettingsRepository)
return ShouldShowPromoWalletUseCase(promoRepository, settingsRepository)
}
@Provides

View file

@ -54,8 +54,8 @@ internal object SettingsDomainModule {
@Singleton
fun providesShouldShowSaveWalletScreenUseCase(
settingsRepository: SettingsRepository,
): ShouldShowSaveWalletScreenUseCase {
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
): ShouldShowAskBiometryUseCase {
return ShouldShowAskBiometryUseCase(settingsRepository = settingsRepository)
}
@Provides
@ -138,10 +138,8 @@ internal object SettingsDomainModule {
@Provides
@Singleton
fun provideSetSaveWalletScreenShownUseCase(
settingsRepository: SettingsRepository,
): SetSaveWalletScreenShownUseCase {
return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository)
fun provideSetSaveWalletScreenShownUseCase(settingsRepository: SettingsRepository): SetAskBiometryShownUseCase {
return SetAskBiometryShownUseCase(settingsRepository = settingsRepository)
}
@Provides

View file

@ -47,7 +47,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
@ -56,7 +55,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -111,13 +109,11 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -156,7 +152,6 @@ internal object TokensDomainModule {
dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
@ -165,7 +160,6 @@ internal object TokensDomainModule {
currencyChecksRepository = currencyChecksRepository,
currencyStatusOperations = baseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -177,7 +171,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
@ -186,7 +179,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -214,9 +206,8 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCryptoCurrencyUseCase {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles)
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@ -238,13 +229,11 @@ internal object TokensDomainModule {
fun provideApplyTokenListSortingUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -304,14 +293,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): IsCryptoCurrencyCoinCouldHideUseCase {
return IsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -330,13 +315,11 @@ internal object TokensDomainModule {
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetBalanceNotEnoughForFeeWarningUseCase {
return GetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -377,16 +360,12 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -402,18 +381,13 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
@ -422,15 +396,10 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -6,8 +6,6 @@ import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -63,18 +61,14 @@ internal object TransactionDomainModule {
fun provideAssociateAssetUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -196,8 +190,13 @@ internal object TransactionDomainModule {
fun providePrepareAndSignUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): PrepareAndSignUseCase {
return PrepareAndSignUseCase(transactionRepository, cardSdkConfigRepository)
return PrepareAndSignUseCase(
transactionRepository = transactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
}
@Provides
@ -241,4 +240,13 @@ internal object TransactionDomainModule {
): GetReverseResolvedEnsAddressUseCase {
return GetReverseResolvedEnsAddressUseCase(walletAddressServiceRepository)
}
@Provides
@Singleton
fun sendLargeSolanaTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
): SendLargeSolanaTransactionUseCase {
return SendLargeSolanaTransactionUseCase(cardSdkConfigRepository, walletManagersFacade)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -9,8 +10,9 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
@ -25,7 +27,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@ -133,6 +135,20 @@ internal object WalletsDomainModule {
)
}
@Provides
@Singleton
fun providesIsWalletAlreadySavedUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): IsWalletAlreadySavedUseCase {
return IsWalletAlreadySavedUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase {
@ -352,4 +368,84 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesIsUpgradeWalletNotificationEnabledUseCase(
walletsRepository: WalletsRepository,
): IsUpgradeWalletNotificationEnabledUseCase {
return IsUpgradeWalletNotificationEnabledUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesDismissUpgradeWalletNotificationUseCase(
walletsRepository: WalletsRepository,
): DismissUpgradeWalletNotificationUseCase {
return DismissUpgradeWalletNotificationUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesUnlockHotWalletContextualUseCase(
hotWalletAccessor: HotWalletAccessor,
): UnlockHotWalletContextualUseCase {
return UnlockHotWalletContextualUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesGetHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): GetHotWalletContextualUnlockUseCase {
return GetHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesClearHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): ClearHotWalletContextualUnlockUseCase {
return ClearHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesClearAllHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): ClearAllHotWalletContextualUnlockUseCase {
return ClearAllHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesExportSeedPhraseUseCase(hotWalletAccessor: HotWalletAccessor): ExportSeedPhraseUseCase {
return ExportSeedPhraseUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesColdWalletInteractionNeededUseCase(
derivationsRepository: DerivationsRepository,
getUserWalletUseCase: GetUserWalletUseCase,
): ColdWalletAndHasMissedDerivationsUseCase {
return ColdWalletAndHasMissedDerivationsUseCase(
derivationsRepository = derivationsRepository,
userWalletUseCase = getUserWalletUseCase,
)
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.di.domain
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
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 YieldSupplyDomainModule {
@Provides
@Singleton
fun provideYieldSupplyStartEarningUseCase(
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
): YieldSupplyStartEarningUseCase {
return YieldSupplyStartEarningUseCase(
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyStopEarningUseCase(
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
): YieldSupplyStopEarningUseCase {
return YieldSupplyStopEarningUseCase(
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyEstimateEnterFeeUseCase(
feeRepository: FeeRepository,
feeErrorResolver: FeeErrorResolver,
): YieldSupplyEstimateEnterFeeUseCase {
return YieldSupplyEstimateEnterFeeUseCase(
feeRepository = feeRepository,
feeErrorResolver = feeErrorResolver,
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di.hot
import com.tangem.data.wallets.hot.DefaultHotWalletAccessor
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds
@ -15,4 +17,8 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletAccessor(default: DefaultHotWalletAccessor): HotWalletAccessor
}

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