Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 12:43:59 +03:00
commit fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions

View file

@ -80,6 +80,7 @@ configurations.androidTestImplementation {
dependencies { dependencies {
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk) implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.core) implementation(projects.domain.core)
implementation(projects.domain.card) implementation(projects.domain.card)
@ -144,6 +145,7 @@ dependencies {
implementation(projects.libs.blockchainSdk) implementation(projects.libs.blockchainSdk)
implementation(projects.libs.tangemSdkApi) implementation(projects.libs.tangemSdkApi)
implementation(projects.data.account)
implementation(projects.data.appCurrency) implementation(projects.data.appCurrency)
implementation(projects.data.appTheme) implementation(projects.data.appTheme)
implementation(projects.data.balanceHiding) implementation(projects.data.balanceHiding)
@ -226,13 +228,21 @@ dependencies {
implementation(projects.features.usedesk.impl) implementation(projects.features.usedesk.impl)
implementation(projects.features.hotWallet.api) implementation(projects.features.hotWallet.api)
implementation(projects.features.hotWallet.impl) implementation(projects.features.hotWallet.impl)
implementation(projects.features.kyc.api)
//TODO disable for release because of the permissions //TODO disable for release because of the permissions
// implementation(projects.features.kyc.api)
// implementation(projects.features.kyc.impl) // implementation(projects.features.kyc.impl)
implementation(projects.features.welcome.api) implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl) implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.api)
implementation(projects.features.createWalletSelection.impl) implementation(projects.features.createWalletSelection.impl)
implementation(projects.features.home.api)
implementation(projects.features.home.impl)
implementation(projects.features.account.api)
implementation(projects.features.account.impl)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.tangempay.details.impl)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.main.impl)
/** AndroidX libraries */ /** AndroidX libraries */
implementation(deps.androidx.core.ktx) implementation(deps.androidx.core.ktx)
@ -255,13 +265,11 @@ dependencies {
/** Compose libraries */ /** Compose libraries */
implementation(deps.compose.constraintLayout) implementation(deps.compose.constraintLayout)
implementation(deps.compose.material)
implementation(deps.compose.material3) implementation(deps.compose.material3)
implementation(deps.compose.animation) implementation(deps.compose.animation)
implementation(deps.compose.coil) implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout) implementation(deps.compose.constraintLayout)
implementation(deps.compose.foundation) implementation(deps.compose.foundation)
implementation(deps.compose.material)
implementation(deps.compose.navigation.hilt) implementation(deps.compose.navigation.hilt)
implementation(deps.compose.shimmer) implementation(deps.compose.shimmer)
implementation(deps.compose.ui) implementation(deps.compose.ui)
@ -346,7 +354,7 @@ dependencies {
/** Chucker */ /** Chucker */
debugImplementation(deps.chucker) debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub) mockedImplementation(deps.chucker)
externalImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub)

View file

@ -15,8 +15,11 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.allure.FailedStepScreenshotInterceptor
import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager 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.tap.MainActivity import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import kotlinx.coroutines.runBlocking
import org.junit.Rule import org.junit.Rule
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.rules.TestRule import org.junit.rules.TestRule
@ -40,6 +43,9 @@ abstract class BaseTestCase : TestCase(
@Inject @Inject
lateinit var apiConfigsManager: ApiConfigsManager lateinit var apiConfigsManager: ApiConfigsManager
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
private val hiltRule = HiltAndroidRule(this) private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule() private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant( private val permissionRule = GrantPermissionRule.grant(
@ -73,6 +79,14 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection: () -> Unit = {}, additionalAfterSection: () -> Unit = {},
) = before { ) = before {
hiltRule.inject() hiltRule.inject()
runBlocking {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.set(
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
value = false
)
}
}
apiEnvironmentRule.setup(apiConfigsManager) apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
Intents.init() Intents.init()

View file

@ -0,0 +1,7 @@
package com.tangem.common.constants
object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val WAIT_UNTIL_TIMEOUT = 20_000L
}

View file

@ -0,0 +1,17 @@
package com.tangem.common.extensions
import com.tangem.common.BaseTestCase
fun BaseTestCase.swipeUp(
startHeightRatio: Float = 0.8f,
endHeightRatio: Float = 0.03f,
steps: Int = 15
) {
device.uiDevice.swipe(
device.uiDevice.displayWidth / 2,
(device.uiDevice.displayHeight * startHeightRatio).toInt(),
device.uiDevice.displayWidth / 2,
(device.uiDevice.displayHeight * endHeightRatio).toInt(),
steps
)
}

View file

@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule {
ApiConfig.ID.TangemTech, ApiConfig.ID.TangemTech,
ApiConfig.ID.Express, ApiConfig.ID.Express,
ApiConfig.ID.TangemPay, ApiConfig.ID.TangemPay,
ApiConfig.ID.StakeKit,
) )
} }
} }

View file

@ -0,0 +1,119 @@
package com.tangem.common.utils
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.IOException
/**
* Method uses to set WireMock scenario state
* @param scenarioName Name of the scenario to modify
* @param state The target state to set (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL
* @return true if state was set successfully, false otherwise
*/
fun setWireMockScenarioState(
scenarioName: String,
state: String,
baseUrl: String = "[REDACTED_ENV_URL]"
): Boolean {
Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state")
val client = OkHttpClient()
val json = """{"state": "$state"}"""
val mediaType = "application/json".toMediaType()
val request = Request.Builder()
.url("$baseUrl/__admin/scenarios/$scenarioName/state")
.put(json.toRequestBody(mediaType))
.build()
return try {
client.newCall(request).execute().use { response ->
val body = response.body?.string() ?: ""
Timber.d("WireMock scenario request URL: ${request.url}")
Timber.d("WireMock scenario request body: $json")
Timber.d("WireMock scenario response: ${response.code} - ${response.message}")
Timber.d("WireMock scenario response body: $body")
response.isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "WireMock scenario error")
false
}
}
/**
* Method checks accessibility of WireMock
*/
fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/__admin/scenarios")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
val body = response.body?.string() ?: ""
Timber.d("WireMock status check: ${response.code}")
Timber.d("Available scenarios: $body")
response.isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "WireMock not accessible")
false
}
}
/**
* Method to reset all WireMock scenarios
*/
fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
Timber.i("=== WireMock Scenarios Reset ===")
Timber.i("Base URL: $baseUrl")
val client = OkHttpClient()
val url = "$baseUrl/__admin/scenarios/reset"
Timber.i("Request URL: $url")
val request = Request.Builder()
.url(url)
.post("".toRequestBody())
.build()
return try {
Timber.d("Sending reset request...")
client.newCall(request).execute().use { response ->
Timber.d("Response code: ${response.code}")
Timber.d("Response message: ${response.message}")
val responseBody = response.body?.string() ?: ""
Timber.d("Response body: $responseBody")
val isSuccessful = response.isSuccessful
Timber.d("Is successful: $isSuccessful")
isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "Exception during reset")
false
}
}
/**
* Method to reset a specific WireMock scenario to its initial state
* @param scenarioName Name of the scenario to reset
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL
* @return true if reset was successful, false otherwise
*/
fun resetWireMockScenarioState(
scenarioName: String,
initialState: String = "Started",
baseUrl: String = "[REDACTED_ENV_URL]"
): Boolean {
Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
return setWireMockScenarioState(scenarioName, initialState, baseUrl)
}

View file

@ -0,0 +1,101 @@
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.BuyTokenDetailsScreenTestTags
import com.tangem.core.ui.test.NotificationTestTags
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
import androidx.compose.ui.test.hasTestTag as withTestTag
class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BuyTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
val topBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val topBarMoreButton: KNode = child {
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
useUnmergedTree = true
}
val topBarCloseButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val errorNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.common_error))
useUnmergedTree = true
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
hasText(getResourceString(R.string.common_unknown_error))
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.warning_button_refresh))
}
val fiatCurrencyIcon: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON)
useUnmergedTree = true
}
val expandFiatListButton: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON)
useUnmergedTree = true
}
val fiatAmountTextField: KNode = child {
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD))
useUnmergedTree = true
}
val tokenAmountField: KNode = child {
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT))
useUnmergedTree = true
}
val providerLoadingTitle: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE)
}
val providerLoadingText: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT)
}
val providerTitle: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE)
useUnmergedTree = true
}
val providerText: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT)
useUnmergedTree = true
}
val buyButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_buy))
}
val toSBlock: KNode = child {
hasTestTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onBuyTokenDetailsScreen(function: BuyTokenDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,38 @@
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.BuyTokenFiatListTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
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
class BuyTokenFiatListPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BuyTokenFiatListPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(BuyTokenFiatListTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
fun fiatListItemWithTitle(title: String): KNode {
return lazyList.child<KNode> {
hasText(title)
}
}
}
internal fun BaseTestCase.onBuyTokenFiatListBottomSheet(function: BuyTokenFiatListPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,54 @@
package com.tangem.screens
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.common.utils.LazyListItemNode
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
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 BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_buy))
useUnmergedTree = true
}
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(BuyTokenScreenTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitleAndFiatAmount(tokenTitle: String): KNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,6 +3,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R 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.DialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen 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.ComposeScreen.Companion.onComposeScreen
@ -17,14 +18,19 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
} }
val cancelButton: KNode = child { val cancelButton: KNode = child {
hasTestTag(DialogTestTags.BUTTON) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel)) hasText(getResourceString(R.string.common_cancel))
} }
val hideButton: KNode = child { val hideButton: KNode = child {
hasTestTag(DialogTestTags.BUTTON) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.token_details_hide_alert_hide)) hasText(getResourceString(R.string.token_details_hide_alert_hide))
} }
val confirmButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_confirm))
}
} }
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) = internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =

View file

@ -3,9 +3,12 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.DisclaimerScreenTestTags import com.tangem.core.ui.test.DisclaimerScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.features.disclaimer.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen 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.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DisclaimerPageObject>( ComposeScreen<DisclaimerPageObject>(
@ -13,6 +16,15 @@ class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
viewBuilderAction = { hasTestTag(DisclaimerScreenTestTags.SCREEN_CONTAINER) } viewBuilderAction = { hasTestTag(DisclaimerScreenTestTags.SCREEN_CONTAINER) }
) { ) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.disclaimer_title))
}
val webView: KNode = child {
hasTestTag(DisclaimerScreenTestTags.WEB_VIEW)
}
val acceptButton: KNode = child { val acceptButton: KNode = child {
hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON) hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
} }

View file

@ -39,6 +39,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(R.string.common_generate_addresses)) hasText(getResourceString(R.string.common_generate_addresses))
} }
val buyButton: KNode = child {
hasTestTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy))
}
/** /**
* Find token list item with title and address * Find token list item with title and address
*/ */

View file

@ -0,0 +1,84 @@
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.ReferralProgramScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
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 com.tangem.feature.referral.presentation.R as ReferralPresentationR
import androidx.compose.ui.test.hasText as withText
class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ReferralProgramPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.details_referral_title))
useUnmergedTree = true
}
val referTitle: KNode = child {
hasText(getResourceString(R.string.referral_title))
useUnmergedTree = true
}
val image: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.IMAGE)
useUnmergedTree = true
}
val infoForYouText: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT)
useUnmergedTree = true
}
val infoForYouBlock: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK)
hasAnyDescendant(
withText(
getResourceString(ReferralPresentationR.string.referral_point_currencies_title),
substring = true
)
)
useUnmergedTree = true
}
val infoForYourFriendText: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOUR_FRIEND_TEXT)
useUnmergedTree = true
}
val infoForYourFriendBlock: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK)
hasAnyDescendant(
withText(
getResourceString(ReferralPresentationR.string.referral_point_discount_title),
substring = true
)
)
useUnmergedTree = true
}
val agreementText: KNode = child {
hasText(getResourceString(
ReferralPresentationR.string.referral_tos_not_enroled_prefix) + " " +
getResourceString(ReferralPresentationR.string.common_terms_and_conditions ) + " " +
getResourceString(ReferralPresentationR.string.referral_tos_suffix),
substring = true
)
useUnmergedTree = true
}
val participateButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,46 @@
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.ResidenceSettingsScreenTestTags
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
import com.tangem.features.onramp.impl.R as OnrampImplR
class ResidenceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ResidenceSettingsPageObject>(semanticsProvider = semanticsProvider) {
val topBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.onramp_settings_title))
useUnmergedTree = true
}
val topBarCloseButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val residenceButton: KNode = child {
hasText(getResourceString(OnrampImplR.string.onramp_settings_residence))
useUnmergedTree = true
}
val countryName: KNode = child {
hasTestTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME)
useUnmergedTree = true
}
val residenceSettingsDescription: KNode = child {
hasText(getResourceString(OnrampImplR.string.onramp_settings_residence_description))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onResidenceSettingsScreen(function: ResidenceSettingsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,59 @@
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.SelectCountryBottomSheetTestTags
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
import androidx.compose.ui.test.hasTestTag as withTestTag
class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectCountryPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(SelectCountryBottomSheetTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val searchBar: KNode = child {
hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR)
useUnmergedTree = true
}
fun countryWithNameAndIcon(name: String): KNode {
return lazyList.child<KNode> {
hasText(name)
hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON))
useUnmergedTree = true
}
}
fun unavailableCountryWithNameAndIcon(name: String): KNode {
return lazyList.child<KNode> {
hasText(name)
hasAnySibling(withText(getResourceString(R.string.onramp_country_unavailable)))
hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON))
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onSelectCountryBottomSheet(function: SelectCountryPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,42 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectNetworkFeePageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_fee_selector_title))
useUnmergedTree = true
}
val marketSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
useUnmergedTree = true
}
val fastSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
useUnmergedTree = true
}
val readMoreTextBlock: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,50 @@
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.SelectPaymentMethodBottomSheetTestTags
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.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class SelectPaymentMethodPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectPaymentMethodPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.onramp_pay_with))
}
fun paymentMethodWithNameAndIcon(name: String): KNode {
return lazyList.child<KNode> {
hasAnyDescendant(withText(name))
hasAnyDescendant(withTestTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON))
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onSelectPaymentMethodBottomSheet(function: SelectPaymentMethodPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,91 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags
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.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class SelectProviderPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectProviderPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.onramp_choose_provider_title_hint))
useUnmergedTree = true
}
val paymentMethodIcon: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON)
useUnmergedTree = true
}
val paymentMethodTitle: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME)
useUnmergedTree = true
}
val paymentMethodName: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME)
useUnmergedTree = true
}
val paymentMethodExpandButton: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON)
useUnmergedTree = true
}
val availableProviderItem: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM)
useUnmergedTree = true
}
val availableProviderName: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME)
useUnmergedTree = true
}
val tokenAmount: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT)
useUnmergedTree = true
}
val unavailableProviderItem: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM)
useUnmergedTree = true
}
val unavailableProviderName: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME)
useUnmergedTree = true
}
val moreProvidersIcon: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON)
useUnmergedTree = true
}
val moreProvidersText: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT)
useUnmergedTree = true
}
val bestRateLabel: KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL)
useUnmergedTree = true
}
fun availableProviderWithName(name: String, tokenAmount: String, rate: String): KNode = child {
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM)
hasAnyChild(withText(name))
hasAnyChild(withText(tokenAmount))
hasAnyChild(withText(rate))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSelectProviderBottomSheet(function: SelectProviderPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,117 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.*
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 com.tangem.features.staking.impl.R as StakingImplR
import androidx.compose.ui.test.hasTestTag as withTestTag
class StakingDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingDetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
}
val stakingTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val bannerImage: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.BANNER_IMAGE)
useUnmergedTree = true
}
val bannerText: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.BANNER_TEXT)
useUnmergedTree = true
}
val annualPercentageRate: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_annual_percentage_rate))
useUnmergedTree = true
}
val availableBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_available))
useUnmergedTree = true
}
val unbondingPeriodBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_unbonding_period))
useUnmergedTree = true
}
val rewardClaimingBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_reward_claiming))
useUnmergedTree = true
}
val rewardScheduleBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_reward_schedule))
useUnmergedTree = true
}
val rewardsBlock: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK)
useUnmergedTree = true
}
val rewardsBlockTitle: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK_TITLE)
useUnmergedTree = true
}
val rewardsBlockText: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK_TEXT)
useUnmergedTree = true
}
val yourStakesTitle: KNode = child {
hasText(getResourceString(StakingImplR.string.staking_your_stakes))
useUnmergedTree = true
}
val activeStakingBlock: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK)
useUnmergedTree = true
}
val toSText: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.TOS_TEXT)
useUnmergedTree = true
}
val stakeMoreButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.staking_stake_more))
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingDetailsScreen(function: StakingDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,51 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
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 StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendDetailsPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val validatorBlock: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK)
useUnmergedTree = true
}
val networkFeeBlock: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK)
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,78 @@
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.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
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) {
val screenContainer: KNode = child {
hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER)
}
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val amountContainerTitle: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE)
useUnmergedTree = true
}
val amountContainerText: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT)
useUnmergedTree = true
}
val amountInputTextField: KNode = child {
hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val currencyButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON))
useUnmergedTree = true
}
val fiatButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON))
useUnmergedTree = true
}
val maxButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true
}
val previousButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_next))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,20 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
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 SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapStoriesPageObject>(semanticsProvider = semanticsProvider) {
val closeButton: KNode = child {
hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,77 @@
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.*
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
class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapTokenPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_swap))
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
val textInput: KNode = child {
hasParent(withTestTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD))
useUnmergedTree = true
}
val networkFeeBlock: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK)
useUnmergedTree = true
}
val receiveAmountShimmer: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
}
val swapTokensOnscreenButton: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON)
}
val receiveAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD)
useUnmergedTree = true
}
val providersBlock: KNode = child {
hasTestTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK)
useUnmergedTree = true
}
val errorNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
useUnmergedTree = true
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.warning_button_refresh))
}
val swapButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_swap))
}
}
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,11 +1,19 @@
package com.tangem.screens 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.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.features.tokendetails.impl.R
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import io.github.kakaocup.compose.node.element.ComposeScreen 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.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode 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
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
@ -13,6 +21,96 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
val screenContainer: KNode = child { val screenContainer: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
} }
val availableStakingBlock: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK)
useUnmergedTree = true
}
val stakingBlock: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_BLOCK)
useUnmergedTree = true
}
val availableStakingBlockTitle: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE)
useUnmergedTree = true
}
val availableStakingBlockText: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT)
useUnmergedTree = true
}
val availableStakingBlockCurrencyIcon: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON)
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
val stakingFiatAmount: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT)
useUnmergedTree = true
}
val stakingDot: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT)
useUnmergedTree = true
}
val stakingTokenAmount: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
useUnmergedTree = true
}
val stakingChevronIcon: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON)
useUnmergedTree = true
}
val stakingTitle: KNode = child {
hasText(getResourceString(R.string.staking_native))
}
val title: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
}
private val horizontalActionChips = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
@OptIn(ExperimentalTestApi::class)
val swapButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_swap))
}
@OptIn(ExperimentalTestApi::class)
val sellButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_sell))
}
@OptIn(ExperimentalTestApi::class)
val buyButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy))
}
} }
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) = internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =

View file

@ -0,0 +1,499 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
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 dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class BuyTokenTest : BaseTestCase() {
@AllureId("3478")
@DisplayName("Onramp: error in providers loading")
@Test
fun errorInProvidersLoadingTest() {
val scenarioName = "payment_methods"
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Setup WireMock scenario '$scenarioName' for 'Error' state") {
setWireMockScenarioState(scenarioName, "Error")
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Assert error notification title is displayed") {
onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() }
}
step("Assert error notification text is displayed") {
onBuyTokenDetailsScreen { errorNotificationText.assertIsDisplayed() }
}
step("Assert 'Refresh' button is displayed and clickable") {
onBuyTokenDetailsScreen { refreshButton.clickWithAssertion() }
}
}
}
@AllureId("2565")
@DisplayName("Onramp: validate currency selector")
@Test
fun validateCurrencySelectorTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val popularFiatsTitle = "Popular Fiats"
val otherCurrenciesTitle = "Other currencies"
val australianDollar = "AUD"
val fiatAmount = "1"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() }
}
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert 'Provider loading block' is displayed") {
onBuyTokenDetailsScreen {
providerLoadingTitle.assertIsDisplayed()
providerLoadingText.assertIsDisplayed()
}
}
step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen {
providerTitle.assertIsDisplayed()
providerText.assertIsDisplayed()
}
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen {
tokenAmountField.assertTextContains(tokenAmount)
}
}
step("Fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
}
step("Click on 'Expand fiat list' button") {
onBuyTokenDetailsScreen { expandFiatListButton.clickWithAssertion() }
}
step("Assert '$popularFiatsTitle' is displayed") {
onBuyTokenFiatListBottomSheet {
fiatListItemWithTitle(popularFiatsTitle).assertIsDisplayed()
}
}
step("Assert '$otherCurrenciesTitle' is displayed") {
onBuyTokenFiatListBottomSheet {
fiatListItemWithTitle(otherCurrenciesTitle).assertIsDisplayed()
}
}
step("Click on fiat with title: '$australianDollar'") {
onBuyTokenFiatListBottomSheet {
fiatListItemWithTitle(australianDollar).performClick()
}
}
step("Assert new fiat currency: '$australianDollar' is displayed") {
onBuyTokenDetailsScreen {
fiatAmountTextField.assertTextContains(australianDollar + fiatAmount)
}
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen {
tokenAmountField.assertTextContains(tokenAmount)
}
}
}
}
@AllureId("2566")
@DisplayName("Onramp: validate 'Buy token' screen")
@Test
fun validateBuyTokenScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val euro = "EUR"
val fiatAmount = "1"
val tokenAmount = "POL 488.24938338"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() }
}
step("Assert 'Buy Token' title is displayed") {
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
}
step("Assert 'More button' in top bar is displayed") {
onBuyTokenDetailsScreen { topBarMoreButton.assertIsDisplayed() }
}
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) }
}
step("Assert 'Provider loading block' is displayed") {
onBuyTokenDetailsScreen {
providerLoadingTitle.assertIsDisplayed()
providerLoadingText.assertIsDisplayed()
}
}
step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen {
providerTitle.assertIsDisplayed()
providerText.assertIsDisplayed()
}
}
step("Assert token amount = '$tokenAmount'") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) }
}
step("Assert 'ToS' block is displayed") {
onBuyTokenDetailsScreen { toSBlock.assertIsDisplayed()}
}
step("Assert 'Buy' button is displayed") {
onBuyTokenDetailsScreen { buyButton.assertIsDisplayed()}
}
step("Assert 'Close' button in top bar is displayed") {
onBuyTokenDetailsScreen { topBarCloseButton.assertIsDisplayed() }
}
}
}
@AllureId("2563")
@DisplayName("Onramp: validate 'Residence' settings screen")
@Test
fun validateResidenceSettingsScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val country = "Albania"
val unavailableCountry = "Lebanon"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() }
}
step("Assert 'Buy $tokenTitle' title is displayed") {
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
}
step("Click 'More' button in tab bar") {
onBuyTokenDetailsScreen { topBarMoreButton.clickWithAssertion() }
}
step("Assert 'Residence Settings' screen top bar title is displayed") {
onResidenceSettingsScreen { topBarTitle.assertIsDisplayed() }
}
step("Assert 'Residence Settings' screen top bar 'Close' button is displayed") {
onResidenceSettingsScreen { topBarCloseButton.assertIsDisplayed() }
}
step("Assert 'Residence' button is displayed on 'Residence Settings' screen") {
onResidenceSettingsScreen { residenceButton.assertIsDisplayed() }
}
step("Assert country name is displayed on 'Residence Settings' screen") {
onResidenceSettingsScreen { countryName.assertIsDisplayed() }
}
step("Assert residence settings description is displayed on 'Residence Settings' screen") {
onResidenceSettingsScreen { residenceSettingsDescription.assertIsDisplayed() }
}
step("Click 'Residence button'") {
onResidenceSettingsScreen { residenceButton.clickWithAssertion() }
}
step("Assert 'Search bar' is displayed") {
onSelectCountryBottomSheet { searchBar.assertIsDisplayed() }
}
step("Type unavailable country name: '$unavailableCountry' in 'Search bar'") {
onSelectCountryBottomSheet { searchBar.performTextReplacement(unavailableCountry) }
}
step("Unavailable country: '$unavailableCountry' is displayed") {
onSelectCountryBottomSheet { unavailableCountryWithNameAndIcon(unavailableCountry).assertIsDisplayed() }
}
step("Type country name: '$country' in 'Search bar'") {
onSelectCountryBottomSheet { searchBar.performTextReplacement(country) }
}
step("Available country: '$country' is displayed") {
onSelectCountryBottomSheet { countryWithNameAndIcon(country).assertIsDisplayed() }
}
step("Click on country: '$country'") {
onSelectCountryBottomSheet { countryWithNameAndIcon(country).clickWithAssertion() }
}
step("Assert country: '$country' is displayed on 'Residence Settings' screen") {
onResidenceSettingsScreen { countryName.assertTextContains(country) }
}
}
}
@AllureId("2570")
@DisplayName("Onramp: validate 'Select provider' bottom sheet")
@Test
fun validateProvidersScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val paymentMethod = "Card"
val fiatAmount = "1"
val providerNameMercuryo = "Mercuryo"
val providerNameSimplex = "Simplex"
val tokenAmount = "POL 488.24938338"
val bestRate = "Best rate"
val rate = "-0.00%"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() }
}
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen {
providerTitle.assertIsDisplayed()
providerText.assertIsDisplayed()
}
}
step("Open 'Select Provider' bottom sheet") {
onBuyTokenDetailsScreen { providerTitle.performClick() }
}
step("Assert available provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(WAIT_UNTIL_TIMEOUT) {
availableProviderItem.assertIsDisplayed()
}
}
}
step("Click on 'Expand payment methods' button") {
onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() }
}
step("Click on payment method: '$paymentMethod'") {
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(paymentMethod).clickWithAssertion() }
}
step("Assert 'Select Provider' bottom sheet title is displayed") {
onSelectProviderBottomSheet { title.assertIsDisplayed() }
}
step("Assert payment method icon is displayed") {
onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() }
}
step("Assert payment method title is displayed") {
onSelectProviderBottomSheet { paymentMethodTitle.assertIsDisplayed() }
}
step("Assert payment method name is displayed") {
onSelectProviderBottomSheet { paymentMethodName.assertIsDisplayed() }
}
step("Assert provider with name: '$providerNameMercuryo' and rate: '$bestRate' is displayed") {
onSelectProviderBottomSheet {
availableProviderWithName(providerNameMercuryo, tokenAmount, bestRate).assertIsDisplayed()
}
}
step("Assert provider with name: '$providerNameSimplex' and rate: '$rate' is displayed") {
onSelectProviderBottomSheet {
availableProviderWithName(providerNameSimplex, tokenAmount, rate).assertIsDisplayed()
}
}
step("Assert 'More providers' icon is displayed") {
onSelectProviderBottomSheet { moreProvidersIcon.assertIsDisplayed() }
}
step("Assert 'More providers' text is displayed") {
onSelectProviderBottomSheet { moreProvidersText.assertIsDisplayed() }
}
step("Assert 'Best rate' label is displayed") {
onSelectProviderBottomSheet { bestRateLabel.assertIsDisplayed() }
}
}
}
@AllureId("3479")
@DisplayName("Onramp: validate 'Select payment method' bottom sheet")
@Test
fun validatePaymentMethodScreenTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
val card = "Card"
val googlePay = "Google Pay"
val invoiceRevolutPay = "Invoice Revolut Pay"
val sepa = "Sepa"
val fiatAmount = "1"
val scenarioName = "payment_methods"
step("Reset WireMock scenario '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onBuyTokenScreen {
topAppBarTitle.assertIsDisplayed()
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
}
}
step("Click on 'Confirm' button in 'Dialog'") {
onDialog { confirmButton.clickWithAssertion() }
}
step("Write fiat amount = '$fiatAmount'") {
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
}
step("Assert 'Provider block' is displayed") {
onBuyTokenDetailsScreen {
providerTitle.assertIsDisplayed()
providerText.assertIsDisplayed()
}
}
step("Open 'Select Provider' bottom sheet") {
onBuyTokenDetailsScreen { providerTitle.performClick() }
}
step("Click on 'Expand payment methods' button") {
onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() }
}
step("Assert 'Select Payment Method' bottom sheet title is displayed") {
onSelectPaymentMethodBottomSheet { title.assertIsDisplayed() }
}
step("Assert payment method: '$card' is displayed") {
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(card).assertIsDisplayed() }
}
step("Assert payment method: '$googlePay' is displayed") {
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(googlePay).assertIsDisplayed() }
}
step("Assert payment method: '$invoiceRevolutPay' is displayed") {
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(invoiceRevolutPay).assertIsDisplayed() }
}
step("Assert payment method: '$sepa' is displayed") {
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(sepa).assertIsDisplayed() }
}
step("Press 'Back' button") {
onSelectPaymentMethodBottomSheet { device.uiDevice.pressBack() }
}
step("Assert 'Select Provider' bottom sheet title is displayed") {
onSelectProviderBottomSheet { title.assertIsDisplayed() }
}
}
}
}

View file

@ -5,9 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.onDetailsScreen import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onReferralProgramScreen
import com.tangem.screens.onTopBar import com.tangem.screens.onTopBar
import com.tangem.screens.onWalletSettingsScreen import com.tangem.screens.onWalletSettingsScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -61,7 +64,7 @@ class DetailsTest : BaseTestCase() {
} }
} }
@Test // @Test
fun wallet2DetailsTest() = fun wallet2DetailsTest() =
setupHooks().run { setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
@ -153,4 +156,48 @@ class DetailsTest : BaseTestCase() {
} }
} }
} }
@AllureId("3647")
@DisplayName("Referral program: validate screen")
@Test
fun validateReferralProgramScreenTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open wallet details") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
step("Click on 'Referral program' button ") {
onWalletSettingsScreen { referralProgramButton.clickWithAssertion() }
}
step("Assert 'Referral program' screen title is displayed") {
onReferralProgramScreen { title.assertIsDisplayed() }
}
step("Assert 'Referral program' screen image is displayed") {
onReferralProgramScreen { image.assertIsDisplayed() }
}
step("Assert 'Referral program' screen refer title is displayed") {
onReferralProgramScreen { referTitle.assertIsDisplayed() }
}
step("Assert info for you title is displayed") {
onReferralProgramScreen { infoForYouText.assertIsDisplayed() }
}
step("Assert info for you text is displayed") {
onReferralProgramScreen { infoForYouBlock.assertIsDisplayed() }
}
step("Assert info for your friend title is displayed") {
onReferralProgramScreen { infoForYourFriendText.assertIsDisplayed() }
}
step("Assert info for your friend text is displayed") {
onReferralProgramScreen { infoForYourFriendBlock.assertIsDisplayed() }
}
step("Assert agreement text is displayed") {
onReferralProgramScreen { agreementText.assertIsDisplayed() }
}
step("Assert 'Participate' button is displayed") {
onReferralProgramScreen { participateButton.assertIsDisplayed() }
}
}
} }

View file

@ -1,6 +1,7 @@
package com.tangem.tests package com.tangem.tests
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.* import com.tangem.screens.*
@ -17,7 +18,7 @@ class HideTokenTest : BaseTestCase() {
@Test @Test
fun hideWalletTokenByHideButtonTest() { fun hideWalletTokenByHideButtonTest() {
val tokenTitle = "Polygon" val tokenTitle = "Polygon"
val balance = "<$0.01" val balance = TOTAL_BALANCE
setupHooks().run { setupHooks().run {
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule)) scenario(OpenMainScreenScenario(composeTestRule))

View file

@ -2,7 +2,9 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.onMainScreen import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen import com.tangem.screens.onOrganizeTokensScreen
@ -27,6 +29,10 @@ class OrganizeTokensTest : BaseTestCase() {
step("Click on 'Synchronize addresses' button" ) { step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() } onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
} }
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") { step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() } onMainScreen { organizeTokensButton().clickWithAssertion() }
} }
@ -48,6 +54,10 @@ class OrganizeTokensTest : BaseTestCase() {
step("Assert tokens were grouped on 'Main screen'") { step("Assert tokens were grouped on 'Main screen'") {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() } onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
} }
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") { step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() } onMainScreen { organizeTokensButton().clickWithAssertion() }
} }
@ -79,18 +89,26 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run { setupHooks().run {
val ethereumTitle = "Ethereum" val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin" val bitcoinTitle = "Bitcoin"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule)) scenario(OpenMainScreenScenario(composeTestRule))
} }
step("Click on 'Synchronize addresses' button" ) { step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() } onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
} }
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Check positions of tokens on 'Main Screen'") { step("Check positions of tokens on 'Main Screen'") {
onMainScreen { onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
} }
} }
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") { step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() } onMainScreen { organizeTokensButton().clickWithAssertion() }
} }
@ -123,6 +141,10 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
} }
} }
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") { step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() } onMainScreen { organizeTokensButton().clickWithAssertion() }
} }
@ -154,12 +176,17 @@ class OrganizeTokensTest : BaseTestCase() {
val ethereumTitle = "Ethereum" val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin" val bitcoinTitle = "Bitcoin"
val polygonTitle = "Polygon" val polygonTitle = "Polygon"
val polExMaticTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule)) scenario(OpenMainScreenScenario(composeTestRule))
} }
step("Click on 'Synchronize addresses' button" ) { step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() } onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
} }
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Check positions of tokens on 'Main Screen'") { step("Check positions of tokens on 'Main Screen'") {
onMainScreen { onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
@ -167,6 +194,10 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
} }
} }
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") { step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() } onMainScreen { organizeTokensButton().clickWithAssertion() }
} }
@ -175,6 +206,7 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 4).assertIsDisplayed()
} }
} }
step("Click 'By Balance' button") { step("Click 'By Balance' button") {
@ -185,8 +217,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Check positions of tokens by balance on 'Organize tokens' screen") { step("Check positions of tokens by balance on 'Organize tokens' screen") {
onOrganizeTokensScreen { onOrganizeTokensScreen {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(polExMaticTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 4).assertIsDisplayed()
} }
} }
step("Click 'Apply' button") { step("Click 'Apply' button") {
@ -195,8 +228,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Check positions of tokens by balance on 'Organize tokens' screen") { step("Check positions of tokens by balance on 'Organize tokens' screen") {
onMainScreen { onMainScreen {
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(polExMaticTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
} }
} }
} }

View file

@ -0,0 +1,379 @@
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.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
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 StakingTest : BaseTestCase() {
@AllureId("3558")
@DisplayName("Staking: validate staking block on 'Token details' screen")
@Test
fun validateStakingBlockTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Staking block' is displayed") {
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
}
step("Assert 'Staking title' is displayed") {
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert 'Staking fiat amount' is displayed") {
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
}
step("Assert 'Staking dot' is displayed") {
onTokenDetailsScreen { stakingDot.assertIsDisplayed() }
}
step("Assert 'Staking token amount' is displayed") {
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
}
step("Assert 'Staking block chevron icon' is displayed") {
onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() }
}
}
}
@AllureId("3550")
@DisplayName("Staking: validate staking more screens")
@Test
fun validateStakingMoreScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
val stakingAmount = "1"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click on 'Staking block'") {
onTokenDetailsScreen { stakingBlock.clickWithAssertion() }
}
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' is displayed") {
onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' title is displayed") {
onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() }
}
step("Assert 'Rewards block' text is displayed") {
onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() }
}
step("Assert 'Active staking block' is displayed") {
onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() }
}
step("Assert 'Your stakes' title is displayed") {
onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake more' button is displayed") {
onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() }
}
step("Click 'Stake more' button") {
onStakingDetailsScreen { stakeMoreButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
}
}
}
@AllureId("3548")
@DisplayName("Staking: validate staking screens")
@Test
fun validateStakingScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Started"
val stakingAmount = "1"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Available staking block' is displayed") {
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
}
step("Assert 'Available staking block' title is displayed") {
onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() }
}
step("Assert 'Available staking block' text is displayed") {
onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() }
}
step("Assert 'Available staking block' currency icon is displayed") {
onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() }
}
step("Click on 'Stake' button") {
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
}
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert banner image is displayed") {
onStakingDetailsScreen { bannerImage.assertIsDisplayed() }
}
step("Assert banner text is displayed") {
onStakingDetailsScreen { bannerText.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingDetailsScreen { stakeButton.assertIsDisplayed() }
}
step("Click 'Stake' button") {
onStakingDetailsScreen { stakeButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
}
}
}
}

View file

@ -5,17 +5,16 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDisclaimerScreen import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onStoriesScreen import com.tangem.screens.onStoriesScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.intent.KIntent import io.github.kakaocup.kakao.intent.KIntent
import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
class StoriesTest : BaseTestCase() { class StoriesTest : BaseTestCase() {
@Test // @Test
fun clickOnOrderButtonTest() = fun clickOnOrderButtonTest() =
setupHooks().run { setupHooks().run {
val buyWalletUrl = "https://buy.tangem.com/"
onDisclaimerScreen { onDisclaimerScreen {
step("Click on 'Accept' button") { step("Click on 'Accept' button") {
acceptButton.clickWithAssertion() acceptButton.clickWithAssertion()
@ -28,7 +27,7 @@ class StoriesTest : BaseTestCase() {
step("Assert: browser opened") { step("Assert: browser opened") {
val expectedIntent = KIntent { val expectedIntent = KIntent {
hasAction(ACTION_VIEW) hasAction(ACTION_VIEW)
hasData { toString().startsWith(NEW_BUY_WALLET_URL) } hasData { toString().startsWith(buyWalletUrl) }
} }
expectedIntent.intended() expectedIntent.intended()
device.uiDevice.pressBack() device.uiDevice.pressBack()

View file

@ -0,0 +1,240 @@
package com.tangem.tests
import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
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 dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SwapTokenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@DisplayName("Swap: network fee")
@Test
fun networkFeeTest() {
val inputAmount = "100"
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onSwapTokenScreen { closeButton.assertIsDisplayed() }
}
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Input swap amount = '$inputAmount'") {
composeTestRule.waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Assert 'Providers' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
providersBlock.assertIsDisplayed()
}
}
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.assertIsDisplayed()
}
}
}
step("Assert receive amount is not equal to '0'") {
onSwapTokenScreen { receiveAmount.assert(!hasText("0")) }
}
}
}
@AllureId("3549")
@DisplayName("Swap: network error test")
@Test
fun networkErrorSwapTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert error notification title is displayed") {
onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() }
}
step("Assert error notification text is displayed") {
onSwapTokenScreen { errorNotificationText.assertIsDisplayed() }
}
step("Assert 'Refresh' button is displayed") {
onSwapTokenScreen { refreshButton.assertIsDisplayed() }
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@DisplayName("Swap: change network fee")
@Test
fun changeNetworkFeeTest() {
val inputAmount = "100"
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Input swap amount = '$inputAmount'") {
composeTestRule.waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Click on 'Network fee' block") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.clickWithAssertion()
}
}
}
step("Assert 'Select fee' bottom sheet title is displayed") {
onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Market' item is displayed") {
onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
}
step("Assert 'Fast' item is displayed") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Read more' text block is displayed") {
onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
}
step("Click on 'Fast' item") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.assertIsDisplayed()
}
}
}
}
}
}

View file

@ -0,0 +1,106 @@
package com.tangem.tests
import androidx.test.InstrumentationRegistry.getTargetContext
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onStoriesScreen
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 TermsOfServiceTest : BaseTestCase() {
@AllureId("3573")
@DisplayName("ToS: success acceptance")
@Test
fun validateTermsOfServiceScreenTest() {
setupHooks().run {
val tosUrl = "https://tangem.com/tangem_tos.html"
step("Assert title of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { title.assertIsDisplayed() }
}
step("Assert title of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { webView.assertIsDisplayed() }
}
step("Verify WebView loads correct URL") {
onDisclaimerScreen {
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
}
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
}
}
}
@AllureId("3574")
@DisplayName("ToS: accept after app restart")
@Test
fun acceptTermsOfServiceAfterAppRestart() {
val packageName = getTargetContext().packageName
setupHooks().run {
val tosUrl = "https://tangem.com/tangem_tos.html"
step("Assert title of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { title.assertIsDisplayed() }
}
step("Assert WebView of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { webView.assertIsDisplayed() }
}
step("Verify WebView loads correct URL") {
onDisclaimerScreen {
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
}
}
step("'Accept' button is displayed") {
onDisclaimerScreen { acceptButton.assertIsDisplayed() }
}
step("Open recent apps") {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
}
step("Assert title of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { title.assertIsDisplayed() }
}
step("Assert WebView of 'Disclaimer screen' is displayed") {
onDisclaimerScreen { webView.assertIsDisplayed() }
}
step("Verify WebView loads correct URL") {
onDisclaimerScreen {
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
}
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Open recent apps") {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
}
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
}
}
}
}

@ -1 +1 @@
Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository 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.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
@ -142,4 +145,10 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
} }

View file

@ -7,6 +7,8 @@ import androidx.work.WorkerParameters
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import timber.log.Timber import timber.log.Timber
@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor(
@Assisted params: WorkerParameters, @Assisted params: WorkerParameters,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val userWalletsListManager: UserWalletsListManager, private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.i("onStart job") Timber.i("onStart job")
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListManagerLockable.lock() userWalletsListRepository.lockAllWallets()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) .onRight {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
} else {
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
userWalletsListManagerLockable.lock()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
Timber.i("onStart job complete") Timber.i("onStart job complete")
return Result.success() return Result.success()
} }

View file

@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -25,6 +27,8 @@ internal class LockUserWalletsTimer(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val duration: Duration = with(Duration) { 5.minutes }, private val duration: Duration = with(Duration) { 5.minutes },
private val userWalletsListManager: UserWalletsListManager, private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
) : LifecycleOwner by context as LifecycleOwner, ) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver { DefaultLifecycleObserver {
@ -108,20 +112,33 @@ internal class LockUserWalletsTimer(
delay(duration) delay(duration)
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch if (hotWalletFeatureToggles.isHotWalletEnabled) {
val userWallets = userWalletsListRepository.userWalletsSync()
if (userWallets.isNotEmpty()) {
userWalletsListRepository.lockAllWallets()
.onLeft {
start()
}
.onRight {
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
} else {
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (userWalletsListManager.hasUserWallets) { if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
Timber.i( Timber.i(
""" """
Finished Finished
|- Millis passed: ${currentTime - startTime} |- Millis passed: ${currentTime - startTime}
""".trimIndent(), """.trimIndent(),
) )
userWalletsListManager.lock() userWalletsListManager.lock()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
} }
} }
} }

View file

@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
@ -39,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
@ -47,7 +49,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent 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.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper import com.tangem.google.GoogleServicesHelper
@ -175,13 +180,31 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher 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
@Inject
internal lateinit var userWalletsListRepository: UserWalletsListRepository
@Inject
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels() internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode> private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private val dialogManager = DialogManager() private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>() private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -231,7 +254,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
lifecycle.addObserver(defaultDeviceFlipDetector) lifecycle.addObserver(defaultDeviceFlipDetector)
if (BuildConfig.TESTER_MENU_ENABLED) { if (BuildConfig.TESTER_MENU_ENABLED) {
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
} }
} }
@ -261,6 +284,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
coroutineScope = mainScope, coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
hotWalletFeatureToggles = hotWalletFeatureToggles,
) )
initIntentHandlers() initIntentHandlers()
@ -343,12 +368,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
private fun initIntentHandlers() { private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(onPushClickedIntentHandler)
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler()) intentProcessor.addHandler(walletConnectLinkIntentHandler)
} }
} }
@ -409,11 +432,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun dispatchTouchEvent(event: MotionEvent): Boolean { override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
return if (result) super.dispatchTouchEvent(event) else false return if (result) super.dispatchTouchEvent(event) else false
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (BuildConfig.TESTER_MENU_ENABLED) {
testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
} else {
super.dispatchKeyEvent(event)
}
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { 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() val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA]) // TODO move inital navigation to navigation component ([REDACTED_JIRA])
val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial } val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial }
@ -433,10 +469,73 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
} }
@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?) { private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction { store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) replaceAll(AppRoute.TangemPayDetails)
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
} }
intentProcessor.handleIntent( intentProcessor.handleIntent(
intent = intentWhichStartedActivity, intent = intentWhichStartedActivity,
@ -450,7 +549,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val route = if (shouldShowTos) { val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false) AppRoute.Disclaimer(isTosAccepted = false)
} else { } else {
AppRoute.Home AppRoute.Home(launchMode = launchMode)
} }
store.dispatchNavigationAction { replaceAll(route) } store.dispatchNavigationAction { replaceAll(route) }

View file

@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val userTokensResponseStore: UserTokensResponseStore private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore() get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
private val tangemHotSdk
get() = entryPoint.getTangemHotSdk()
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
// endregion // endregion
private val appScope = MainScope() private val appScope = MainScope()
@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
uiMessageSender = uiMessageSender, uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore, userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles,
), ),
), ),
) )

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class IntroductionProcess(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Introduction Process", event, params) {
class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
class ButtonTokensList : IntroductionProcess("Button - Tokens List")
class ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
class ButtonScanCard : IntroductionProcess("Button - Scan Card")
class ButtonRequestSupport : IntroductionProcess("Button - Request Support")
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
*/
sealed class Shop(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {
class ScreenOpened : Shop("Shop Screen Opened")
class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
event = "Purchased",
params = mapOf(
"SKU" to sku,
"Count" to count,
"Amount" to amount,
"Coupon Code" to couponCode,
).filterNotNull(),
)
class Redirected(partnerName: String?) : Shop(
event = "Redirected",
params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(),
)
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState

View file

@ -1,17 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.sdk.extensions.pxToDp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -24,7 +24,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) { fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue) setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY]) // TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) { if (userWallet is UserWallet.Cold) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))

View file

@ -1,3 +0,0 @@
package com.tangem.tap.common.extensions
fun Int.isEven() = this and 1 == 0

View file

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

View file

@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState 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.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -20,7 +18,6 @@ import org.rekotlin.StateType
data class AppState( data class AppState(
val globalState: GlobalState = GlobalState(), val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val detailsState: DetailsState = DetailsState(), val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(), val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(), val welcomeState: WelcomeState = WelcomeState(),
@ -32,7 +29,6 @@ data class AppState(
return listOf( return listOf(
logMiddleware, logMiddleware,
GlobalMiddleware.handler, GlobalMiddleware.handler,
HomeMiddleware.handler,
DetailsMiddleware().detailsMiddleware, DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware, WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware, BackupMiddleware().backupMiddleware,

View file

@ -26,10 +26,9 @@ internal object LegacyMiddleware {
{ action -> { action ->
when (action) { when (action) {
is LegacyAction.PrepareDetailsScreen -> { is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository) val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
userWalletsListManager.selectedUserWallet selectedUserWallet()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { selectedUserWallet -> .onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState( val initializedAppSettingsStateContent = initializeAppSettingsState(
@ -52,6 +51,16 @@ internal object LegacyMiddleware {
} }
} }
private fun selectedUserWallet(): Flow<UserWallet> {
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
} else {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWallet
}
}
/** /**
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
* previously it was initialized in runBlocking and blocked details screen * previously it was initialized in runBlocking and blocked details screen
@ -64,6 +73,8 @@ internal object LegacyMiddleware {
selectedAppCurrency = store.state.globalState.appCurrency, selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT, ?: AppThemeMode.DEFAULT,
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings, .getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
@ -14,8 +15,6 @@ import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
@ -29,6 +28,7 @@ internal object ScanFailsDialog {
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
private const val RUSSIA_LOCALE = "ru"
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
@ -62,8 +62,8 @@ internal object ScanFailsDialog {
source = sourceAnalytics, source = sourceAnalytics,
), ),
) )
val locale = LocaleRegionProvider().getRegion() val locale = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link) store.dispatchOpenUrl(link)
} }
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
// [REDACTED_JIRA] // [REDACTED_JIRA]
@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore(
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
} }
override suspend fun getAllSyncOrNull(): List<UserWallet>? {
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update( override suspend fun update(
userWalletId: UserWalletId, userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet, update: suspend (UserWallet) -> UserWallet,

View file

@ -0,0 +1,50 @@
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.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
class UserWalletsStoreRepositoryProxy(
private val userWalletsListRepository: UserWalletsListRepository,
) : UserWalletsStore {
override val selectedUserWalletOrNull: UserWallet?
get() = userWalletsListRepository.selectedUserWallet.value
override val userWallets: Flow<List<UserWallet>>
get() = flow {
userWalletsListRepository.load()
userWalletsListRepository.userWallets.collect {
emit(requireNotNull(it))
}
}
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListRepository.userWallets.value?.find { it.walletId == key }
}
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return catching {
val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId }
requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" }
val updatedUserWallet = update(userWallet)
userWalletsListRepository.saveWithoutLock(
userWallet = updatedUserWallet,
canOverride = true,
)
updatedUserWallet
}
}
}

View file

@ -0,0 +1,34 @@
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
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
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,34 +0,0 @@
package com.tangem.tap.di.data
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 CardDataModule {
@Singleton
@Provides
fun providesDerivationsRepository(
tangemSdkManager: TangemSdkManager,
userWalletsStore: UserWalletsStore,
networkFactory: NetworkFactory,
dispatchers: CoroutineDispatcherProvider,
): DerivationsRepository {
return DefaultDerivationsRepository(
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
networkFactory = networkFactory,
dispatchers = dispatchers,
)
}
}

View file

@ -2,7 +2,10 @@ package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.data.RuntimeUserWalletsStore import com.tangem.tap.data.RuntimeUserWalletsStore
import com.tangem.tap.data.UserWalletsStoreRepositoryProxy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -15,7 +18,15 @@ internal object UserWalletsStoreModule {
@Provides @Provides
@Singleton @Singleton
fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { fun provideUserWalletsStore(
return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UserWalletsStore {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
UserWalletsStoreRepositoryProxy(userWalletsListRepository)
} else {
RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
}
} }
} }

View file

@ -0,0 +1,52 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.*
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 AccountDomainModule {
@Provides
@Singleton
fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase {
return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideUpdateCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): UpdateCryptoPortfolioUseCase {
return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideArchiveCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideGetUnoccupiedAccountIndexUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): GetUnoccupiedAccountIndexUseCase {
return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository)
}
}

View file

@ -2,13 +2,18 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.* import com.tangem.domain.card.*
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.card.DefaultResetCardUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase
@ -39,9 +44,16 @@ internal object CardDomainModule {
} }
@Provides @Provides
@Singleton fun provideIsNeedToBackupUseCase(
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { userWalletsListManager: UserWalletsListManager,
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides

View file

@ -3,7 +3,9 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import com.tangem.tap.domain.scanCard.LegacyScanProcessor import com.tangem.tap.domain.scanCard.LegacyScanProcessor
@ -31,7 +33,15 @@ internal object CardLegacyDomainModule {
@Provides @Provides
@Singleton @Singleton
fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { fun providesWalletNameGenerateUseCase(
return GenerateWalletNameUseCase(userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
} }

View file

@ -1,11 +1,12 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
@ -72,6 +73,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveManagedTokensUseCase { ): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase( return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository, customTokensRepository = customTokensRepository,
@ -81,6 +83,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
) )
} }

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.markets.* import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
@ -9,9 +9,12 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -63,6 +66,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveMarketTokensUseCase { ): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase( return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
@ -71,6 +75,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -78,10 +83,14 @@ object MarketsDomainModule {
@Singleton @Singleton
fun provideFilterNetworksUseCase( fun provideFilterNetworksUseCase(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
excludedBlockchains: ExcludedBlockchains, excludedBlockchains: ExcludedBlockchains,
): FilterAvailableNetworksForWalletUseCase { ): FilterAvailableNetworksForWalletUseCase {
return FilterAvailableNetworksForWalletUseCase( return FilterAvailableNetworksForWalletUseCase(
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
excludedBlockchains = excludedBlockchains, excludedBlockchains = excludedBlockchains,
) )
} }

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.* import com.tangem.domain.notifications.*
import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.repository.PushNotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
import com.tangem.utils.notifications.PushNotificationsTokenProvider import com.tangem.utils.notifications.PushNotificationsTokenProvider
@ -18,20 +19,22 @@ internal object NotificationsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase { fun providesGetApplicationIdUseCase(
pushNotificationsRepository: PushNotificationsRepository,
): GetApplicationIdUseCase {
return GetApplicationIdUseCase( return GetApplicationIdUseCase(
notificationsRepository = notificationsRepository, pushNotificationsRepository = pushNotificationsRepository,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun providesSendPushTokenUseCase( fun providesSendPushTokenUseCase(
notificationsRepository: NotificationsRepository, pushNotificationsRepository: PushNotificationsRepository,
pushNotificationsTokenProvider: PushNotificationsTokenProvider, pushNotificationsTokenProvider: PushNotificationsTokenProvider,
): SendPushTokenUseCase { ): SendPushTokenUseCase {
return SendPushTokenUseCase( return SendPushTokenUseCase(
notificationsRepository = notificationsRepository, pushNotificationsRepository = pushNotificationsRepository,
pushNotificationsTokenProvider = pushNotificationsTokenProvider, pushNotificationsTokenProvider = pushNotificationsTokenProvider,
) )
} }
@ -56,6 +59,26 @@ internal object NotificationsDomainModule {
) )
} }
@Provides
@Singleton
fun providesShouldShowNotificationUseCase(
notificationsRepository: NotificationsRepository,
): ShouldShowNotificationUseCase {
return ShouldShowNotificationUseCase(
notificationsRepository = notificationsRepository,
)
}
@Provides
@Singleton
fun providesSetShouldShowNotificationUseCase(
notificationsRepository: NotificationsRepository,
): SetShouldShowNotificationUseCase {
return SetShouldShowNotificationUseCase(
notificationsRepository = notificationsRepository,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
@ -65,8 +88,8 @@ internal object NotificationsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetNetworksAvailableForNotifications( fun provideGetNetworksAvailableForNotifications(
notificationsRepository: NotificationsRepository, pushNotificationsRepository: PushNotificationsRepository,
): GetNetworksAvailableForNotificationsUseCase { ): GetNetworksAvailableForNotificationsUseCase {
return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository) return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository)
} }
} }

View file

@ -94,12 +94,12 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideFetchStakingYieldBalanceUseCase( fun provideFetchStakingYieldBalanceUseCase(
stakingErrorResolver: StakingErrorResolver,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchStakingYieldBalanceUseCase { ): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase( return FetchStakingYieldBalanceUseCase(
stakingErrorResolver = stakingErrorResolver,
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -173,18 +173,6 @@ internal object StakingDomainModule {
) )
} }
@Provides
@Singleton
fun provideIsApproveNeededUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsApproveNeededUseCase {
return IsApproveNeededUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetConstructedStakingTransactionUseCase( fun provideGetConstructedStakingTransactionUseCase(
@ -209,12 +197,6 @@ internal object StakingDomainModule {
) )
} }
@Provides
@Singleton
fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase {
return GetStakingIntegrationIdUseCase(stakingRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideCheckAccountInitializedUseCase( fun provideCheckAccountInitializedUseCase(
@ -225,9 +207,13 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetActionRequirementAmountUseCase( fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase {
stakingRepository: StakingRepository, return GetActionRequirementAmountUseCase()
): GetActionRequirementAmountUseCase { }
return GetActionRequirementAmountUseCase(stakingRepository)
@Provides
@Singleton
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
} }
} }

View file

@ -11,12 +11,13 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -46,6 +47,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase { ): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase( return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
@ -54,6 +56,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -64,12 +67,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase { ): FetchTokenListUseCase {
return FetchTokenListUseCase( return FetchTokenListUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -91,11 +96,11 @@ internal object TokensDomainModule {
@Singleton @Singleton
fun provideGetTokenListUseCase( fun provideGetTokenListUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetTokenListUseCase { ): GetTokenListUseCase {
return GetTokenListUseCase( return GetTokenListUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
currenciesStatusesOperations = baseCurrenciesStatusesOperations, currenciesStatusesOperations = currenciesStatusesOperations,
) )
} }
@ -172,6 +177,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase { ): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase( return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
@ -180,6 +186,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -190,12 +197,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase { ): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase( return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -359,9 +368,9 @@ internal object TokensDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetWalletTotalBalanceUseCase( fun provideGetWalletTotalBalanceUseCase(
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetWalletTotalBalanceUseCase { ): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
} }
@Provides @Provides
@ -389,47 +398,12 @@ internal object TokensDomainModule {
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
} }
@Provides
@Singleton
fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideBaseCurrencyStatusOperations( fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -437,13 +411,14 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations { ): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations( return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -451,9 +426,11 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
) )
} }
@ -472,6 +449,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher { ): WalletBalanceFetcher {
return WalletBalanceFetcher( return WalletBalanceFetcher(
@ -481,6 +459,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.data.wallets.hot.TangemHotWalletSigner
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
@ -11,7 +12,6 @@ import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.hot.TangemHotWalletSigner
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -181,8 +181,13 @@ internal object TransactionDomainModule {
fun providePrepareForSendUseCase( fun providePrepareForSendUseCase(
transactionRepository: TransactionRepository, transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): PrepareForSendUseCase { ): PrepareForSendUseCase {
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository) return PrepareForSendUseCase(
transactionRepository = transactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
} }
@Provides @Provides
@ -199,8 +204,13 @@ internal object TransactionDomainModule {
fun provideSignUseCase( fun provideSignUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SignUseCase { ): SignUseCase {
return SignUseCase(cardSdkConfigRepository, walletManagersFacade) return SignUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
} }
@Provides @Provides

View file

@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager 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.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.* import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -31,18 +33,30 @@ internal object WalletsDomainModule {
@Provides @Provides
fun providesUserWalletsSyncDelegate( fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate { ): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate( return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { fun providesGetWalletsUseCase(
return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletsUseCase {
return GetWalletsUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@ -50,37 +64,79 @@ internal object WalletsDomainModule {
fun providesWalletNameMigrationUseCase( fun providesWalletNameMigrationUseCase(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
walletNamesMigrationRepository: WalletNamesMigrationRepository, walletNamesMigrationRepository: WalletNamesMigrationRepository,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): WalletNameMigrationUseCase { ): WalletNameMigrationUseCase {
return WalletNameMigrationUseCase( return WalletNameMigrationUseCase(
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
walletNamesMigrationRepository = walletNamesMigrationRepository, walletNamesMigrationRepository = walletNamesMigrationRepository,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { fun providesGetUserWalletUseCase(
return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetUserWalletUseCase {
return GetUserWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@Singleton @Singleton
fun providesGetSelectedWalletSyncUseCase( fun providesGetSelectedWalletSyncUseCase(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletSyncUseCase { ): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) return GetSelectedWalletSyncUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@Singleton @Singleton
fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { fun providesGetSelectedWalletUseCase(
return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@Singleton @Singleton
fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { fun providesSaveWalletUseCase(
return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
walletsRepository: WalletsRepository,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase {
return GenerateBuyTangemCardLinkUseCase()
} }
@Provides @Provides
@ -99,15 +155,30 @@ internal object WalletsDomainModule {
@Singleton @Singleton
fun providesSelectWalletUseCase( fun providesSelectWalletUseCase(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
reduxStateHolder: ReduxStateHolder, reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase { ): SelectWalletUseCase {
return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) return SelectWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
reduxStateHolder = reduxStateHolder,
)
} }
@Provides @Provides
@Singleton @Singleton
fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { fun providesUpdateWalletUseCase(
return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UpdateWalletUseCase {
return UpdateWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@ -124,14 +195,30 @@ internal object WalletsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { fun providesGetWalletsSyncUseCase(
return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletNamesUseCase {
return GetWalletNamesUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@Singleton @Singleton
fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { fun providesDeleteWalletUseCase(
return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): DeleteWalletUseCase {
return DeleteWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
} }
@Provides @Provides
@ -214,9 +301,13 @@ internal object WalletsDomainModule {
@Singleton @Singleton
fun providesGetSavedWalletChangesIdUseCase( fun providesGetSavedWalletChangesIdUseCase(
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSavedWalletsCountUseCase { ): GetSavedWalletsCountUseCase {
return GetSavedWalletsCountUseCase( return GetSavedWalletsCountUseCase(
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
) )
} }

View file

@ -1,8 +1,6 @@
package com.tangem.tap.di.hot package com.tangem.tap.di.hot
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester
import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -17,8 +15,4 @@ internal interface TangemHotSdkModule {
@Binds @Binds
@Singleton @Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
} }

View file

@ -1,113 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.common.core.TangemSdkError
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.exception.WrongPasswordException
import com.tangem.hot.sdk.model.*
import javax.inject.Inject
class HotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
) {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
HotWalletId.AuthType.Password -> requestPassword(false)
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
}
return runCatchingSdkErrors(hotWalletId, auth) {
tangemHotSdk.signHashes(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = it,
),
dataToSign = dataToSign,
).also {
hotWalletPasswordRequester.dismiss()
}
}
}
private suspend fun <T> runCatchingSdkErrors(
hotWalletId: HotWalletId,
auth: HotAuth,
block: suspend (auth: HotAuth) -> T,
): T {
return runCatchingWrongPassInternal(
originalAuth = auth,
auth = auth,
block = { blockAuth ->
block(blockAuth).also {
// TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = blockAuth,
),
auth = HotAuth.Biometry,
)
}
}
},
)
}
private suspend fun <T> runCatchingWrongPassInternal(
originalAuth: HotAuth,
auth: HotAuth,
block: suspend (auth: HotAuth) -> T,
): T = runCatching {
block(auth)
}.getOrElse { exception ->
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
// fallback to password if biometry fails
val passAuth = requestPassword(true)
return@getOrElse runCatchingWrongPassInternal(
originalAuth = originalAuth,
auth = passAuth,
block = block,
)
}
if (exception !is WrongPasswordException) {
throw exception
}
// If the exception is a wrong password, we need to request the password again
hotWalletPasswordRequester.wrongPassword()
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
runCatchingWrongPassInternal(
originalAuth = originalAuth,
auth = passResult,
block = block,
)
}
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
}
private fun Throwable.isBiometryError(): Boolean {
return this is TangemSdkError.AuthenticationFailed ||
this is TangemSdkError.AuthenticationCanceled ||
this is TangemSdkError.AuthenticationLockout ||
this is TangemSdkError.AuthenticationUnavailable ||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
this is TangemSdkError.AuthenticationNotInitialized ||
this is TangemSdkError.AuthenticationPermanentLockout
}
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
HotWalletPasswordRequester.Result.Dismiss -> null
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
interface HotWalletPasswordRequester {
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
}

View file

@ -1,78 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.map
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.operations.sign.SignData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class TangemHotSigner @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Hot,
private val hotWalletAccessor: HotWalletAccessor,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
return sign(listOf(hash), publicKey).map { it.first() }
}
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = listOf(
DataToSign(
curve = wallet.curve,
hashes = hashes,
derivationPath = publicKey.derivationPath,
),
),
)
return CompletionResult.Success(result.map { it.signatures }.flatten())
}
override suspend fun multiSign(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = dataToSign.map { signData ->
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
DataToSign(
curve = wallet.curve,
hashes = listOf(signData.hash),
derivationPath = signData.derivationPath,
)
},
)
return CompletionResult.Success(
result.mapIndexed { index, data ->
dataToSign[index].publicKey to data.signatures.first()
}.toMap(),
)
}
@AssistedFactory
interface Factory {
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
}
}

View file

@ -20,13 +20,11 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager(
): CompletionResult<VisaSignedDataByCustomerWallet> { ): CompletionResult<VisaSignedDataByCustomerWallet> {
return runTaskAsyncReturnOnMain( return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask( runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove, VisaCustomerWalletApproveTask.Input(
cardId = visaDataForApprove.customerWalletCardId,
targetAddress = visaDataForApprove.targetAddress,
hashToSign = visaDataForApprove.dataToSign.hashToSign,
sign = visaDataForApprove.dataToSign::sign,
),
), ),
cardId = visaDataForApprove.customerWalletCardId, cardId = visaDataForApprove.customerWalletCardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),

View file

@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse

View file

@ -14,7 +14,7 @@ import com.tangem.common.map
import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId

View file

@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.common.TwinsHelper import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.card.Card import com.tangem.common.card.Card
@ -10,27 +11,24 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashCommand
class VisaCustomerWalletApproveTask( class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove, private val visaDataForApprove: Input,
) : CardSessionRunnable<VisaSignedDataByCustomerWallet> { ) : CardSessionRunnable<VisaSignedDataByCustomerWallet> {
override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) { override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) {
@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask(
return return
} }
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) {
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return return
} }
@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask(
) )
} }
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
private fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
private fun signApproveData( private fun signApproveData(
targetWalletPublicKey: ByteArray, targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?, derivationPath: DerivationPath?,
@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask(
session: CardSession, session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>, callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) { ) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes() val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signTask = SignHashCommand( val signTask = SignHashCommand(
hash = hashToSign, hash = hash,
walletPublicKey = targetWalletPublicKey, walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath, derivationPath = derivationPath,
) )
@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask(
is CompletionResult.Success -> { is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature, signature = result.data.signature,
hash = hashToSign, hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(), ?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase() ).asRSVLegacyEVM().toHexString().lowercase()
@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask(
scanCard( scanCard(
session = session, session = session,
callback = callback, callback = callback,
signedData = visaDataForApprove.dataToSign.sign( signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
) )
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask(
} }
} }
} }
data class Input(
val cardId: String? = null,
val targetAddress: String,
val hashToSign: String,
val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet,
)
} }

View file

@ -11,14 +11,19 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
@ -26,6 +31,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -40,6 +46,7 @@ internal object UserWalletsListManagerModule {
@Provides @Provides
@Singleton @Singleton
@Deprecated("Use UserWalletsListRepository instead")
fun provideGeneralUserWalletsListManager( fun provideGeneralUserWalletsListManager(
@ApplicationContext applicationContext: Context, @ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
@ -58,42 +65,14 @@ internal object UserWalletsListManagerModule {
) )
} }
@Deprecated("Use UserWalletsListRepository instead")
private fun createBiometricUserWalletsListManager( private fun createBiometricUserWalletsListManager(
applicationContext: Context, applicationContext: Context,
analyticsEventHandler: AnalyticsEventHandler, analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): UserWalletsListManager { ): UserWalletsListManager {
val moshi = Moshi.Builder() val moshi = buildMoshi()
.add(WalletDerivedKeysMapAdapter()) val secureStorage = buildSecureStorage(applicationContext = applicationContext)
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
val authenticatedStorage = AuthenticatedStorage( val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator( secureStorage = UserWalletsKeysStoreDecorator(
@ -134,4 +113,97 @@ internal object UserWalletsListManagerModule {
selectedUserWalletRepository = selectedUserWalletRepository, selectedUserWalletRepository = selectedUserWalletRepository,
) )
} }
@Provides
@Singleton
fun provideUserWalletsListRepository(
@ApplicationContext applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
passwordRequester: HotWalletPasswordRequester,
appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
dispatchers = dispatchers,
)
val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository(
moshi = moshi,
authenticatedStorage = authenticatedStorage,
dispatchers = dispatchers,
secureStorage = secureStorage,
)
return DefaultUserWalletsListRepository(
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
passwordRequester = passwordRequester,
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
tangemSdkManagerProvider = Provider { tangemSdkManager },
appPreferencesStore = appPreferencesStore,
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
)
}
fun buildMoshi(): Moshi {
return Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
}
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
return AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
}
} }

View file

@ -0,0 +1,420 @@
package com.tangem.tap.domain.userWalletList.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.core.wallets.error.DeleteWalletError
import com.tangem.domain.core.wallets.error.LockWalletsError
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.core.wallets.error.SetLockError
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lock
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@Suppress("LongParameterList", "LargeClass")
internal class DefaultUserWalletsListRepository(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
private val passwordRequester: HotWalletPasswordRequester,
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
private val savePersistentInformation: ProviderSuspend<Boolean>,
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
override val selectedUserWallet = MutableStateFlow<UserWallet?>(null)
override suspend fun load() {
if (userWallets.value != null) return
if (savePersistentInformation().not()) {
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
userWallets.value = emptyList()
return
}
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { wallets ->
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
.map { wallets.updateWith(it) }
}.doOnSuccess {
userWallets.value = it
}
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId }
?: userWallets.value?.firstOrNull()
}
override suspend fun userWalletsSync(): List<UserWallet> {
load()
return requireNotNull(userWallets.value) {
"This should never happen"
}
}
override suspend fun selectedUserWalletSync(): UserWallet? {
load()
return selectedUserWallet.value
}
override suspend fun select(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SelectWalletError.UnableToSelectUserWallet)
selectedUserWalletRepository.set(userWalletId)
selectedUserWallet.value = userWallet
userWallet
}
override suspend fun saveWithoutLock(
userWallet: UserWallet,
canOverride: Boolean,
): Either<SaveWalletError, UserWallet> = either {
if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) {
raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved))
}
if (savePersistentInformation()) {
publicInformationRepository.save(userWallet, canOverride)
if (userWallet.isLocked.not()) {
sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey)
}
}
// update the userWallets state and add if it doesn't exist
userWallets.update { currentWallets ->
val wallets = currentWallets ?: emptyList()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
} else {
wallets + userWallet
}
}
// update the selectedUserWallet state if it is the only wallet
if (userWallets.value?.size == 1) {
selectedUserWalletRepository.set(userWallet.walletId)
selectedUserWallet.value = userWallet
}
userWallet
}
override suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean,
): Either<SetLockError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
removeUnsecured = changeUnsecured,
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
override suspend fun removeBiometricLock(userWalletId: UserWalletId) {
userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
if (userWalletIds.isEmpty()) return Unit.right()
publicInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
sensitiveInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
userWalletEncryptionKeysRepository.delete(userWalletIds)
val userWalletsBeforeDelete = userWallets.value ?: return@either
userWallets.update { currentWallets ->
currentWallets?.filterNot { it.walletId in userWalletIds }
}
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
userWallets.value?.findAvailableUserWallet(
userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
}
}
override suspend fun unlock(
userWalletId: UserWalletId,
unlockMethod: UserWalletsListRepository.UnlockMethod,
): Either<UnlockWalletError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(UnlockWalletError.UserWalletNotFound)
if (userWallet.isLocked.not()) {
raise(UnlockWalletError.AlreadyUnlocked)
}
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets().bind()
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
if (userWallet !is UserWallet.Hot) {
raise(UnlockWalletError.UnableToUnlock)
}
val encryptionKey = requestPasswordRecursive(
hotWalletId = userWallet.hotWalletId,
block = { password ->
runCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
}.onFailure {
raise(UnlockWalletError.UnableToUnlock)
}.getOrNull()
},
biometryFallback = {
unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
},
).bind()
if (encryptionKey == null) {
return@either
}
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
}
UserWalletsListRepository.UnlockMethod.Scan -> {
if (userWallet !is UserWallet.Cold) {
raise(UnlockWalletError.UnableToUnlock)
}
tangemSdkManagerProvider().scanProduct()
.doOnSuccess { scanResponse ->
val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (expectedId != userWallet.walletId) {
raise(UnlockWalletError.ScannedCardWalletNotMatched)
}
saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true)
.mapLeft { UnlockWalletError.UnableToUnlock }
.bind()
}
.doOnFailure {
raise(UnlockWalletError.UserCancelled)
}
}
}
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWalletIds = userWalletsSync().map { it.walletId }.toSet()
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
// TODO handle error properly [REDACTED_TASK_KEY]
raise(UnlockWalletError.UserCancelled)
}
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
val allKeys = (biometricKeys + unsecuredKeys).distinct()
val unlockedWalletsIds = allKeys.map { it.walletId }
val unlockedWallets = unlockedWalletsIds.mapNotNull { id ->
userWalletsSync().firstOrNull { it.walletId == id }
}
// Remove all password attempts for unlocked hot wallets
unlockedWallets.forEach {
removePasswordAttempts(it)
}
// if we cant unlock all wallets
if (userWalletIds.all { it in unlockedWalletsIds }.not()) {
raise(UnlockWalletError.UnableToUnlock)
}
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
userWallets.update { it?.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
override suspend fun lockAllWallets(): Either<LockWalletsError, Unit> = either {
val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet()
if (unsecuredWalletIds.size == userWallets.value?.size) {
raise(LockWalletsError.NothingToLock)
}
userWallets.update {
it?.map {
if (it.walletId !in unsecuredWalletIds) {
it.lock()
} else {
it
}
}
}
}
override suspend fun clearPersistentData() {
publicInformationRepository.clear()
sensitiveInformationRepository.clear()
userWalletEncryptionKeysRepository.clear()
}
private suspend fun requestPasswordRecursive(
hotWalletId: HotWalletId,
block: suspend (CharArray) -> UserWalletEncryptionKey?,
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
hotWalletId = hotWalletId,
authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts
hasBiometry = hasBiometry(),
)
val result = passwordRequester.requestPassword(attemptRequest)
return when (result) {
HotWalletPasswordRequester.Result.Dismiss -> {
passwordRequester.dismiss()
UnlockWalletError.UserCancelled.left()
}
is HotWalletPasswordRequester.Result.EnteredPassword -> {
val decrypted = block(result.password.value)
if (decrypted == null) {
passwordRequester.wrongPassword()
requestPasswordRecursive(hotWalletId, block, biometryFallback)
} else {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
decrypted.right()
}
}
HotWalletPasswordRequester.Result.UseBiometry -> {
biometryFallback()
.onRight {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
}
.map { null }
}
}
}
private suspend fun removePasswordAttempts(userWallet: UserWallet) {
if (userWallet is UserWallet.Hot) {
hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId)
}
}
private suspend fun hasBiometry(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
default = false,
)
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
/**
* Find the nearest available wallet that can be selected
*
* Example:
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
*
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
*
* @receiver list of user wallets without deleted wallet
*/
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
for (offset in 1..size) {
val rightIndex = prevSelectedIndex + offset
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
val leftIndex = prevSelectedIndex - offset
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
}
return lastOrNull()
}
}

View file

@ -0,0 +1,196 @@
package com.tangem.tap.domain.userWalletList.repository
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class UserWalletEncryptionKeysRepository(
moshi: Moshi,
private val authenticatedStorage: AuthenticatedStorage,
private val dispatchers: CoroutineDispatcherProvider,
private val secureStorage: SecureStorage,
) {
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
UserWalletEncryptionKey::class.java,
)
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
suspend fun save(
encryptionKey: UserWalletEncryptionKey,
removeUnsecured: Boolean = true,
method: EncryptionMethod,
) = withContext(dispatchers.io) {
if (removeUnsecured) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
}
when (method) {
EncryptionMethod.Unsecured -> {
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
is EncryptionMethod.Password -> {
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
password = method.password,
content = encryptionKey.encode(),
)
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name,
data = encodedWithPass,
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
}
storeUserWalletId(userWalletId = encryptionKey.walletId)
}
fun removeBiometricKey(userWalletId: UserWalletId) {
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
getUserWalletsIds().mapNotNull { userWalletId ->
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()
}
}
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? =
withContext(dispatchers.io) {
val encrypted = secureStorage.get(
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
) ?: return@withContext null
withContext(dispatchers.default) {
AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey()
}
}
suspend fun getAllBiometric(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
val keys = getUserWalletsIds().map { userWalletId ->
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
authenticatedStorage.get(keys).mapNotNull {
it.value.decodeToKey()
}
}
suspend fun delete(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
withContext(dispatchers.io) {
userWalletIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds }
secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name)
}
}
suspend fun clear() {
withContext(dispatchers.io) {
val userWalletsIds = getUserWalletsIds()
userWalletsIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
secureStorage.delete(StorageKey.UserWalletIds.name)
}
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.io) {
secureStorage.get(StorageKey.UserWalletIds.name)
.decodeToUserWalletsIds()
}
}
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
withContext(dispatchers.io) {
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
}
}
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(encryptionKeyAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
return withContext(dispatchers.default) {
this@decodeToKey
?.decodeToString(throwOnInvalidSequence = true)
?.let(encryptionKeyAdapter::fromJson)
}
}
private suspend fun List<UserWalletId>.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(userWalletsIdsListAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.default) {
this@decodeToUserWalletsIds
?.decodeToString(throwOnInvalidSequence = true)
?.let(userWalletsIdsListAdapter::fromJson)
.orEmpty()
}
}
sealed class EncryptionMethod {
data object Unsecured : EncryptionMethod()
data object Biometric : EncryptionMethod()
class Password(val password: CharArray) : EncryptionMethod()
}
private sealed interface StorageKey {
val name: String
class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}"
}
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
}
class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}"
}
object UserWalletIds : StorageKey {
override val name: String = "user_wallets_ids_with_saved_keys"
}
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule {
wcSessionsRepository: WalletConnectSessionsRepository, wcSessionsRepository: WalletConnectSessionsRepository,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
walletConnectFeatureToggles: WalletConnectFeatureToggles, walletConnectFeatureToggles: WalletConnectFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider, coroutineDispatcherProvider: CoroutineDispatcherProvider,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
): WalletConnectInteractor { ): WalletConnectInteractor {
return WalletConnectInteractor( return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(), handler = WalletConnectEventsHandlerImpl(),
@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule {
blockchainHelper = TangemWcBlockchainHelper(), blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager, getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = coroutineDispatcherProvider, dispatchers = coroutineDispatcherProvider,
walletConnectFeatureToggles = walletConnectFeatureToggles, walletConnectFeatureToggles = walletConnectFeatureToggles,
) )

View file

@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
@ -38,18 +37,14 @@ class WalletConnectInteractor(
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository, private val currenciesRepository: CurrenciesRepository,
private val userWalletsListManager: UserWalletsListManager,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
val blockchainHelper: WcBlockchainHelper, val blockchainHelper: WcBlockchainHelper,
) { ) {
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
private var isWalletConnectReadyForDeepLinks = false private var isWalletConnectReadyForDeepLinks = false
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedWalletUseCase(userWalletsListManager)
}
private val wcScope = CoroutineScope( private val wcScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")

View file

@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -64,6 +66,14 @@ class DetailsMiddleware {
when (action.setting) { when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode(
state = state,
enable = action.enable,
)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(
state = state,
enable = action.enable,
)
} }
} }
is DetailsAction.AppSettings.CheckBiometricsStatus -> { is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -90,6 +100,91 @@ class DetailsMiddleware {
} }
} }
private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleRequireAccessCode(
state = state,
enable = true,
)
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all biometric sign data
removeAllBiometricSingData()
toggleSaveAccessCodes(state, enable = false)
} else {
toggleSaveAccessCodes(state, enable = true)
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.setLock(
userWalletId = it.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach {
if (it is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = it.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) { private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow { val needEnrollBiometricsFlow = flow {
do { do {
@ -233,7 +328,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes() deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home) } store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit) return CompletionResult.Success(Unit)
} }

View file

@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
) )
} }
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) { return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
saveWallets = true, // User can't enable access codes saving without wallets saving saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = action.enable, saveAccessCodes = action.enable,
) )
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
}, },
) )
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy( is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isInProgress = false, isInProgress = false,
saveAccessCodes = action.prevState, saveAccessCodes = action.prevState,
) )
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
}, },
) )
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(

View file

@ -12,9 +12,14 @@ data class DetailsState(
) : StateType ) : StateType
data class AppSettingsState( data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false, val saveWallets: Boolean = false,
@Deprecated("Delete after hot wallet release")
val saveAccessCodes: Boolean = false, val saveAccessCodes: Boolean = false,
@Deprecated("Delete after hot wallet release")
val isBiometricsAvailable: Boolean = false, val isBiometricsAvailable: Boolean = false,
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false, val needEnrollBiometrics: Boolean = false,
val isHidingEnabled: Boolean = false, val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false, val isInProgress: Boolean = false,
@ -25,5 +30,5 @@ data class AppSettingsState(
enum class SecurityOption { LongTap, PassCode, AccessCode } enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting { enum class AppSetting {
SaveWallets, SaveAccessCode SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
} }

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.appsettings package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R import com.tangem.wallet.R
@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory {
onDismiss = onDismiss, onDismiss = onDismiss,
) )
} }
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
)
}
} }

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.wallet.R import com.tangem.wallet.R
@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory {
) )
} }
fun createUseBiometricsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_USE_BIOMETRICS_SWITCH,
title = resourceReference(R.string.app_settings_enable_biometrics_title),
description = resourceReference(
R.string.app_settings_biometrics_footer,
wrappedList(resourceReference(R.string.common_biometrics)),
),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createRequireAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_REQUIRE_ACCESS_CODE_SWITCH,
title = resourceReference(R.string.app_settings_require_access_code),
description = resourceReference(R.string.app_settings_require_access_code_footer),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createSaveAccessCodeSwitch( fun createSaveAccessCodeSwitch(
isChecked: Boolean, isChecked: Boolean,
isEnabled: Boolean, isEnabled: Boolean,
@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory {
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch" const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button" const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"
const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button" const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button"
const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch"
const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch"
} }
} }

View file

@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository, private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model(), StoreSubscriber<DetailsState> { ) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory() private val itemsFactory = AppSettingsItemsFactory()
@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor(
onClick = ::showAppCurrencySelector, onClick = ::showAppCurrencySelector,
).let(::add) ).let(::add)
if (state.isBiometricsAvailable) { if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch( itemsFactory.createUseBiometricsSwitch(
isChecked = state.saveWallets, isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics, isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled, onCheckedChange = ::onBiometricAuthenticationToggled,
).let(::add) ).let(::add)
itemsFactory.createSaveAccessCodeSwitch( itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.saveAccessCodes, isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics, isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onSaveAccessCodesToggled, onCheckedChange = ::onRequireAccessCodeToggled,
).let(::add) ).let(::add)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
}
} }
itemsFactory.createFlipToHideBalanceSwitch( itemsFactory.createFlipToHideBalanceSwitch(
@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor(
} }
} }
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onRequireAccessCodeToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
)
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) { private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) { if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true) onSettingsToggled(AppSetting.SaveWallets, enable = true)
@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(), saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(), isBiometricsAvailable = canUseBiometryUseCase(),
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,

View file

@ -87,9 +87,10 @@ internal class CardSettingsModel @Inject constructor(
val userWallet = getUserWalletUseCase(userWalletId) val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet $userWalletId not found") } .getOrElse { error("User wallet $userWalletId not found") }
.requireColdWallet()
cardSdkConfigRepository.isBiometricsRequestPolicy = cardSdkConfigRepository.isBiometricsRequestPolicy =
userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY] userWallet.scanResponse.card.isAccessCodeSet &&
settingsRepository.shouldSaveAccessCodes() settingsRepository.shouldSaveAccessCodes()
} }
} }

View file

@ -268,7 +268,7 @@ internal class ResetCardModel @Inject constructor(
if (isLocked && userWalletsListManager.hasUserWallets) { if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() } store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else { } else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home) } store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
} }
} }
} }

View file

@ -1,90 +0,0 @@
package com.tangem.tap.features.home
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
import com.tangem.core.ui.utils.findActivity
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.compose.StoriesScreenV2
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultHomeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
private val model: HomeModel = getOrCreateModel()
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
init {
lifecycle.subscribe(
onCreate = {
store.dispatch(HomeAction.OnCreate)
},
onStart = {
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
},
onStop = {
store.unsubscribe(this)
},
)
}
@Composable
override fun Content(modifier: Modifier) {
val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
StoriesScreenV2(
homeState = homeState,
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
onAddExistingWalletButtonClick = model::onAddExistingWalletScreen,
onScanButtonClick = model::onScanClick,
)
} else {
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
}
ChangeRootBackgroundColorEffect(Color(color = 0xFF010101))
}
override fun newState(state: HomeState) {
homeState.value = state
}
@AssistedFactory
interface Factory : HomeComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent
}
}

View file

@ -1,166 +0,0 @@
package com.tangem.tap.features.home
import androidx.compose.runtime.Stable
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class HomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val router: Router,
getUserCountryUseCase: GetUserCountryUseCase,
) : Model() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
init {
getUserCountryUseCase.invoke()
.distinctUntilChanged()
.filterNotNull()
.onEach {
val userCountry = it.getOrNull() ?: UserCountry.Other(Locale.getDefault().country)
store.dispatchOnMain(HomeAction.UserCountryLoaded(userCountry))
}
.flowOn(dispatchers.io)
.launchIn(modelScope)
}
fun onCreateNewWalletScreen() {
router.push(AppRoute.CreateWalletSelection)
}
fun onAddExistingWalletScreen() {
router.push(AppRoute.AddExistingWallet)
}
fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
scanCard()
}
fun onShopClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
analyticsEventHandler.send(Shop.ScreenOpened())
Firebase.analytics.appInstanceId
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
}
fun onSearchClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) }
}
private fun scanCard() {
modelScope.launch {
cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes()
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onFailure = {
tangemErrorHandler.onErrorReceived(error = it)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = ::proceedWithScanResponse,
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
return
}
saveWalletUseCase(userWallet).fold(
ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") },
ifRight = {
sendSignedInCardAnalyticsEvent(scanResponse)
coroutineScope { store.onUserWalletSelected(userWallet = userWallet) }
},
)
store.dispatchWithMain(HomeAction.ScanInProgress(scanInProgress = false))
delay(HIDE_PROGRESS_DELAY)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
Analytics.send(
event = Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = Basic.SignedIn.SignInType.Card,
walletsCount = "1",
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.features.home
import androidx.compose.ui.text.intl.Locale
/**
[REDACTED_AUTHOR]
*/
interface RegionProvider {
fun getRegion(): String?
}
class LocaleRegionProvider : RegionProvider {
override fun getRegion(): String = Locale.current.region
}
const val RUSSIA_COUNTRY_CODE = "ru"

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.home
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.errors.TangemSdkErrorHandler
import org.rekotlin.Store
import timber.log.Timber
class TangemTangemErrorsHandler(val store: Store<AppState>) : TangemSdkErrorHandler {
override fun onErrorReceived(error: TangemError) {
when (error) {
is TangemSdkError -> {
handleCardSdkError(error)
}
is BlockchainError -> {
handleBlockchainSdkError(error)
}
else -> {
Timber.e("Error happened", error)
}
}
}
private fun handleCardSdkError(error: TangemSdkError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> {
store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable))
}
else -> {
Timber.e(error, "Unable to scan card")
}
}
}
private fun handleBlockchainSdkError(error: TangemError) {
Timber.e("Sdk error happened", error)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.features.home.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface HomeComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, HomeComponent>
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.home.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.home.DefaultHomeComponent
import com.tangem.tap.features.home.HomeModel
import com.tangem.tap.features.home.api.HomeComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface HomeFeatureModule {
@Binds
fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory
@Binds
@IntoMap
@ClassKey(HomeModel::class)
fun bindModel(model: HomeModel): Model
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.features.home.errors
import com.tangem.common.core.TangemError
interface TangemSdkErrorHandler {
fun onErrorReceived(error: TangemError)
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.settings.usercountry.models.UserCountry
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class HomeAction : Action {
data object OnCreate : HomeAction()
/**
* Action for scanning card
*
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
*/
data class ReadCard(val scope: CoroutineScope) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data class UserCountryLoaded(val userCountry: UserCountry) : HomeAction()
}

View file

@ -1,147 +0,0 @@
package com.tangem.tap.features.home.redux
import android.content.res.Resources
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.Locale
internal const val HIDE_PROGRESS_DELAY = 400L
object HomeMiddleware {
val handler = homeMiddleware
private val SYSTEM_LANGUAGE =
runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" }
private val APP_LANGUAGE = Locale.getDefault().language
private val UTM_MARKS = "utm_source=tangem-app" +
"&utm_medium=app" +
"&utm_campaign=prospect-$SYSTEM_LANGUAGE" +
"&utm_content=devicelang-$APP_LANGUAGE"
val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?$UTM_MARKS"
}
private val homeMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
handleHomeAction(action)
next(action)
}
}
}
private fun handleHomeAction(action: Action) {
when (action) {
is HomeAction.OnCreate -> {
Analytics.eraseContext()
Analytics.send(IntroductionProcess.ScreenOpened())
store.dispatch(GlobalAction.RestoreAppCurrency)
}
is HomeAction.ReadCard -> {
action.scope.launch {
readCard()
}
}
}
}
private suspend fun readCard() {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onFailure = {
Timber.e(it, "Unable to scan card")
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build().guard {
Timber.e("User wallet not created")
return@launch
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
sendSignedInCardAnalyticsEvent(scanResponse)
store.onUserWalletSelected(userWallet = userWallet)
}
.doOnResult {
navigateTo(AppRoute.Wallet)
}
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
if (currency != null) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
Analytics.send(
event = Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = Basic.SignedIn.SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private suspend fun navigateTo(route: AppRoute) {
store.dispatchNavigationAction { push(route) }
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.tap.common.redux.AppState
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Action
object HomeReducer {
fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): HomeState {
if (action !is HomeAction) return appState.homeState
return when (action) {
is HomeAction.ScanInProgress -> {
appState.homeState.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.UserCountryLoaded -> {
val stories = if (action.userCountry.needApplyFCARestrictions()) {
getRestrictedStories()
} else {
Stories.entries
}
appState.homeState.copy(
stories = stories.toImmutableList(),
)
}
else -> appState.homeState
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.features.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import javax.inject.Inject
class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester {
override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password {
return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY]
}
}

View file

@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =
callSdk { changeAuth(unlockHotWallet, auth) } callSdk { changeAuth(unlockHotWallet, auth) }
override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId =
callSdk { removeBiometryAuthIfPresented(id) }
override suspend fun derivePublicKey( override suspend fun derivePublicKey(
unlockHotWallet: UnlockHotWallet, unlockHotWallet: UnlockHotWallet,
request: DeriveWalletRequest, request: DeriveWalletRequest,

View file

@ -4,21 +4,12 @@ import android.content.Intent
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.Tag import android.nfc.Tag
import android.os.Build import android.os.Build
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class BackgroundScanIntentHandler( class BackgroundScanIntentHandler {
private val hasSavedUserWalletsProvider: () -> Boolean,
private val scope: CoroutineScope,
) : IntentHandler, AffectsNavigation {
private val nfcActions = arrayOf( private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED, NfcAdapter.ACTION_NDEF_DISCOVERED,
@ -26,8 +17,15 @@ class BackgroundScanIntentHandler(
NfcAdapter.ACTION_TAG_DISCOVERED, NfcAdapter.ACTION_TAG_DISCOVERED,
) )
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode {
if (isFromForeground) return true return if (shouldOpenScanCard(intent)) {
InitScreenLaunchMode.WithCardScan
} else {
InitScreenLaunchMode.Standard
}
}
private fun shouldOpenScanCard(intent: Intent?): Boolean {
if (intent == null || intent.action !in nfcActions) return false if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -36,15 +34,9 @@ class BackgroundScanIntentHandler(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
} }
if (tag == null) return false
intent.action = null intent.action = null
if (hasSavedUserWalletsProvider.invoke()) {
store.dispatchOnMain(WelcomeAction.ProceedWithCard)
} else {
store.dispatchOnMain(HomeAction.ReadCard(scope = scope))
}
return true return tag != null
} }
} }

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